react-native-biometric-verifier 0.0.46 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/hooks/useBluetoothService.js +195 -0
- package/src/hooks/useGeolocation.js +187 -266
- package/src/hooks/useWifiService.js +175 -0
- package/src/index.js +343 -114
package/src/index.js
CHANGED
|
@@ -17,15 +17,17 @@ import {
|
|
|
17
17
|
Animated,
|
|
18
18
|
} from "react-native";
|
|
19
19
|
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
20
|
+
|
|
20
21
|
// Custom hooks
|
|
21
22
|
import { useCountdown } from "./hooks/useCountdown";
|
|
22
23
|
import { useGeolocation } from "./hooks/useGeolocation";
|
|
24
|
+
import { useWifiService } from "./hooks/useWifiService";
|
|
25
|
+
import { useBluetoothService } from "./hooks/useBluetoothService";
|
|
23
26
|
import { useImageProcessing } from "./hooks/useImageProcessing";
|
|
24
27
|
import { useNotifyMessage } from "./hooks/useNotifyMessage";
|
|
25
28
|
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
26
29
|
|
|
27
30
|
// Utils
|
|
28
|
-
import { getDistanceInMeters } from "./utils/distanceCalculator";
|
|
29
31
|
import { Global } from "./utils/Global";
|
|
30
32
|
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
31
33
|
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
@@ -50,32 +52,51 @@ const BiometricModal = forwardRef(({
|
|
|
50
52
|
fileurl,
|
|
51
53
|
imageurl,
|
|
52
54
|
navigation,
|
|
53
|
-
MaxDistanceMeters =
|
|
54
|
-
duration = 100
|
|
55
|
+
MaxDistanceMeters = 30,
|
|
56
|
+
duration = 100,
|
|
57
|
+
useWiFiFingerprinting = true,
|
|
55
58
|
}, ref) => {
|
|
56
|
-
// Custom hooks - Initialize notification hook first
|
|
59
|
+
// Custom hooks - Initialize notification hook first
|
|
57
60
|
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
58
61
|
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
62
|
+
|
|
63
|
+
// Split hooks for geolocation and WiFi
|
|
59
64
|
const {
|
|
60
65
|
requestLocationPermission,
|
|
61
66
|
getCurrentLocation,
|
|
67
|
+
stopLocationWatching,
|
|
68
|
+
calculateSafeDistance,
|
|
69
|
+
} = useGeolocation(notifyMessage);
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
requestWifiPermissions,
|
|
73
|
+
scanWifiFingerprint,
|
|
74
|
+
matchWifiFingerprint,
|
|
75
|
+
getLocationWithWifi,
|
|
76
|
+
} = useWifiService(notifyMessage);
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
requestBluetoothPermission,
|
|
62
80
|
startBluetoothScan,
|
|
63
81
|
stopBluetoothScan,
|
|
64
82
|
nearbyDevices,
|
|
65
|
-
|
|
83
|
+
clearDevices,
|
|
84
|
+
} = useBluetoothService(notifyMessage);
|
|
85
|
+
|
|
66
86
|
const { convertImageToBase64 } = useImageProcessing();
|
|
67
87
|
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
68
88
|
|
|
69
89
|
// State
|
|
70
90
|
const [modalVisible, setModalVisible] = useState(false);
|
|
71
|
-
const [cameraType, setCameraType] = useState("back");
|
|
91
|
+
const [cameraType, setCameraType] = useState("back");
|
|
72
92
|
const [state, setState] = useState({
|
|
73
93
|
isLoading: false,
|
|
74
94
|
loadingType: Global.LoadingTypes.none,
|
|
75
95
|
currentStep: "Start",
|
|
76
96
|
employeeData: null,
|
|
77
|
-
animationState: Global.AnimationStates.qrScan,
|
|
78
|
-
qrData: null,
|
|
97
|
+
animationState: Global.AnimationStates.qrScan,
|
|
98
|
+
qrData: null,
|
|
99
|
+
wifiReferenceScan: null,
|
|
79
100
|
});
|
|
80
101
|
|
|
81
102
|
// Refs
|
|
@@ -84,6 +105,9 @@ const BiometricModal = forwardRef(({
|
|
|
84
105
|
const responseRef = useRef(null);
|
|
85
106
|
const processedRef = useRef(false);
|
|
86
107
|
const resetTimeoutRef = useRef(null);
|
|
108
|
+
const bleScanTimeoutRef = useRef(null);
|
|
109
|
+
const wifiScanTimeoutRef = useRef(null);
|
|
110
|
+
|
|
87
111
|
// Animation values
|
|
88
112
|
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
89
113
|
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
@@ -105,9 +129,20 @@ const BiometricModal = forwardRef(({
|
|
|
105
129
|
clearTimeout(resetTimeoutRef.current);
|
|
106
130
|
}
|
|
107
131
|
|
|
132
|
+
if (bleScanTimeoutRef.current) {
|
|
133
|
+
clearTimeout(bleScanTimeoutRef.current);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (wifiScanTimeoutRef.current) {
|
|
137
|
+
clearTimeout(wifiScanTimeoutRef.current);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stopBluetoothScan();
|
|
141
|
+
stopLocationWatching();
|
|
142
|
+
clearDevices();
|
|
108
143
|
clearNotification();
|
|
109
144
|
};
|
|
110
|
-
}, []);
|
|
145
|
+
}, [stopBluetoothScan, stopLocationWatching, clearDevices, clearNotification]);
|
|
111
146
|
|
|
112
147
|
// Update dataRef when data changes
|
|
113
148
|
useEffect(() => {
|
|
@@ -116,11 +151,9 @@ const BiometricModal = forwardRef(({
|
|
|
116
151
|
|
|
117
152
|
// Animation helper
|
|
118
153
|
const animateIcon = useCallback(() => {
|
|
119
|
-
// Reset animation
|
|
120
154
|
iconScaleAnim.setValue(1);
|
|
121
155
|
iconOpacityAnim.setValue(0);
|
|
122
156
|
|
|
123
|
-
// Start animation sequence
|
|
124
157
|
Animated.sequence([
|
|
125
158
|
Animated.parallel([
|
|
126
159
|
Animated.timing(iconOpacityAnim, {
|
|
@@ -149,7 +182,6 @@ const BiometricModal = forwardRef(({
|
|
|
149
182
|
const merged = { ...prev, ...newState };
|
|
150
183
|
|
|
151
184
|
if (JSON.stringify(prev) !== JSON.stringify(merged)) {
|
|
152
|
-
// Pause/resume countdown based on loading state
|
|
153
185
|
if (newState.isLoading !== undefined) {
|
|
154
186
|
if (newState.isLoading) {
|
|
155
187
|
pauseCountdown();
|
|
@@ -158,7 +190,6 @@ const BiometricModal = forwardRef(({
|
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
192
|
|
|
161
|
-
// Animate icon when step changes
|
|
162
193
|
if (newState.currentStep && newState.currentStep !== prev.currentStep) {
|
|
163
194
|
animateIcon();
|
|
164
195
|
}
|
|
@@ -184,18 +215,22 @@ const BiometricModal = forwardRef(({
|
|
|
184
215
|
employeeData: null,
|
|
185
216
|
animationState: Global.AnimationStates.qrScan,
|
|
186
217
|
qrData: null,
|
|
218
|
+
wifiReferenceScan: null,
|
|
187
219
|
});
|
|
188
220
|
|
|
189
221
|
setModalVisible(false);
|
|
190
222
|
processedRef.current = false;
|
|
191
223
|
resetCountdown();
|
|
224
|
+
stopBluetoothScan();
|
|
225
|
+
stopLocationWatching();
|
|
226
|
+
clearDevices();
|
|
192
227
|
clearNotification();
|
|
193
228
|
|
|
194
229
|
if (resetTimeoutRef.current) {
|
|
195
230
|
clearTimeout(resetTimeoutRef.current);
|
|
196
231
|
resetTimeoutRef.current = null;
|
|
197
232
|
}
|
|
198
|
-
}, [resetCountdown, clearNotification, onclose]);
|
|
233
|
+
}, [resetCountdown, stopBluetoothScan, stopLocationWatching, clearDevices, clearNotification, onclose]);
|
|
199
234
|
|
|
200
235
|
// Error handler
|
|
201
236
|
const handleProcessError = useCallback(
|
|
@@ -241,8 +276,108 @@ const BiometricModal = forwardRef(({
|
|
|
241
276
|
return true;
|
|
242
277
|
}, [apiurl, handleProcessError]);
|
|
243
278
|
|
|
244
|
-
|
|
245
|
-
|
|
279
|
+
/**
|
|
280
|
+
* WiFi fingerprint scanning with validation
|
|
281
|
+
*/
|
|
282
|
+
const performWiFiScan = useCallback(async () => {
|
|
283
|
+
if (!useWiFiFingerprinting || Platform.OS !== 'android') {
|
|
284
|
+
return { scan: [], score: 0, isMatch: false };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
updateState({ loadingType: Global.LoadingTypes.wifiScan });
|
|
289
|
+
|
|
290
|
+
// Request WiFi permissions if needed
|
|
291
|
+
const hasWifiPermission = await requestWifiPermissions();
|
|
292
|
+
if (!hasWifiPermission) {
|
|
293
|
+
return { scan: [], score: 0, isMatch: false };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Perform WiFi scan using the WiFi service hook
|
|
297
|
+
const wifiScan = await scanWifiFingerprint();
|
|
298
|
+
|
|
299
|
+
if (wifiScan.length === 0) {
|
|
300
|
+
notifyMessage("No WiFi networks detected", "warning");
|
|
301
|
+
return { scan: [], score: 0, isMatch: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If we have a reference scan, match against it
|
|
305
|
+
if (state.wifiReferenceScan && state.wifiReferenceScan.length > 0) {
|
|
306
|
+
const matchResult = matchWifiFingerprint(wifiScan, state.wifiReferenceScan);
|
|
307
|
+
const isMatch = matchResult.score >= 3; // Threshold of 3 matching APs
|
|
308
|
+
|
|
309
|
+
if (isMatch) {
|
|
310
|
+
notifyMessage(`WiFi fingerprint match: ${matchResult.score.toFixed(1)}`, "success");
|
|
311
|
+
} else {
|
|
312
|
+
notifyMessage(`Weak WiFi match: ${matchResult.score.toFixed(1)}`, "warning");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
scan: wifiScan,
|
|
317
|
+
score: matchResult.score,
|
|
318
|
+
isMatch,
|
|
319
|
+
matchDetails: matchResult
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Return scan without matching
|
|
324
|
+
return { scan: wifiScan, score: 0, isMatch: false };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.warn("WiFi scan failed:", error);
|
|
327
|
+
notifyMessage("WiFi scan unavailable", "warning");
|
|
328
|
+
return { scan: [], score: 0, isMatch: false };
|
|
329
|
+
}
|
|
330
|
+
}, [
|
|
331
|
+
useWiFiFingerprinting,
|
|
332
|
+
requestWifiPermissions,
|
|
333
|
+
scanWifiFingerprint,
|
|
334
|
+
matchWifiFingerprint,
|
|
335
|
+
state.wifiReferenceScan,
|
|
336
|
+
notifyMessage,
|
|
337
|
+
updateState
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Find consistent BLE devices across multiple samples
|
|
342
|
+
*/
|
|
343
|
+
const findConsistentBLEDevices = useCallback((samples) => {
|
|
344
|
+
const deviceMap = new Map();
|
|
345
|
+
|
|
346
|
+
samples.forEach((sample, sampleIndex) => {
|
|
347
|
+
sample.forEach(device => {
|
|
348
|
+
if (!deviceMap.has(device.id)) {
|
|
349
|
+
deviceMap.set(device.id, {
|
|
350
|
+
...device,
|
|
351
|
+
distances: [],
|
|
352
|
+
samples: 0
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const entry = deviceMap.get(device.id);
|
|
356
|
+
entry.distances.push(parseFloat(device.distance));
|
|
357
|
+
entry.samples++;
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Filter for devices seen in at least 2 samples
|
|
362
|
+
return Array.from(deviceMap.values())
|
|
363
|
+
.filter(device => device.samples >= 2)
|
|
364
|
+
.map(device => ({
|
|
365
|
+
...device,
|
|
366
|
+
avgDistance: device.distances.reduce((a, b) => a + b, 0) / device.distances.length,
|
|
367
|
+
stdDev: calculateStdDev(device.distances)
|
|
368
|
+
}))
|
|
369
|
+
.filter(device => device.stdDev < 2);
|
|
370
|
+
}, []);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Calculate standard deviation
|
|
374
|
+
*/
|
|
375
|
+
const calculateStdDev = (array) => {
|
|
376
|
+
const n = array.length;
|
|
377
|
+
const mean = array.reduce((a, b) => a + b) / n;
|
|
378
|
+
return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
|
|
379
|
+
};
|
|
380
|
+
|
|
246
381
|
const handleQRScanned = useCallback(
|
|
247
382
|
async (qrCodeData) => {
|
|
248
383
|
if (!validateApiUrl()) return;
|
|
@@ -256,12 +391,17 @@ const BiometricModal = forwardRef(({
|
|
|
256
391
|
try {
|
|
257
392
|
// 1. Request permissions
|
|
258
393
|
updateState({ loadingType: Global.LoadingTypes.locationPermission });
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
394
|
+
const hasLocationPermission = await requestLocationPermission();
|
|
395
|
+
if (!hasLocationPermission) {
|
|
261
396
|
handleProcessError("Location permission not granted.");
|
|
262
397
|
return;
|
|
263
398
|
}
|
|
264
399
|
|
|
400
|
+
const hasBluetoothPermission = await requestBluetoothPermission();
|
|
401
|
+
if (!hasBluetoothPermission) {
|
|
402
|
+
notifyMessage("Bluetooth scanning may not work", "warning");
|
|
403
|
+
}
|
|
404
|
+
|
|
265
405
|
// 2. Parse QR data with validation
|
|
266
406
|
const qrString = typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
|
|
267
407
|
if (!qrString || typeof qrString !== "string") {
|
|
@@ -269,7 +409,7 @@ const BiometricModal = forwardRef(({
|
|
|
269
409
|
return;
|
|
270
410
|
}
|
|
271
411
|
|
|
272
|
-
// 3. Parse and validate QR coordinates
|
|
412
|
+
// 3. Parse and validate QR coordinates
|
|
273
413
|
const parts = qrString.split(",");
|
|
274
414
|
|
|
275
415
|
if (parts.length < 3) {
|
|
@@ -279,10 +419,8 @@ const BiometricModal = forwardRef(({
|
|
|
279
419
|
|
|
280
420
|
const latStr = parts[0];
|
|
281
421
|
const lngStr = parts[1];
|
|
282
|
-
const qrDepKey = parts[2];
|
|
283
|
-
|
|
284
|
-
// Optional: QR code accuracy if provided as fourth parameter
|
|
285
|
-
const qrAccuracy = parts.length > 3 ? parseFloat(parts[3]) : 5; // Default 5m accuracy
|
|
422
|
+
const qrDepKey = parts[2];
|
|
423
|
+
const qrAccuracy = parts.length > 3 ? parseFloat(parts[3]) : 5;
|
|
286
424
|
|
|
287
425
|
const qrLat = parseFloat(latStr);
|
|
288
426
|
const qrLng = parseFloat(lngStr);
|
|
@@ -292,45 +430,132 @@ const BiometricModal = forwardRef(({
|
|
|
292
430
|
return;
|
|
293
431
|
}
|
|
294
432
|
|
|
295
|
-
//
|
|
433
|
+
// 4. Perform initial WiFi scan FIRST to establish reference
|
|
434
|
+
let wifiFingerprint = [];
|
|
435
|
+
let wifiMatchScore = 0;
|
|
436
|
+
let isWiFiMatch = false;
|
|
437
|
+
let wifiMatchDetails = null;
|
|
438
|
+
|
|
439
|
+
if (useWiFiFingerprinting && Platform.OS === 'android') {
|
|
440
|
+
try {
|
|
441
|
+
const wifiResult = await performWiFiScan();
|
|
442
|
+
wifiFingerprint = wifiResult.scan;
|
|
443
|
+
|
|
444
|
+
// Store as reference for later matching
|
|
445
|
+
if (wifiFingerprint.length > 0) {
|
|
446
|
+
updateState({ wifiReferenceScan: wifiFingerprint });
|
|
447
|
+
}
|
|
448
|
+
} catch (wifiError) {
|
|
449
|
+
console.warn('WiFi scan failed:', wifiError);
|
|
450
|
+
notifyMessage("WiFi scan failed, continuing without WiFi verification", "warning");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 5. Get high-accuracy location
|
|
296
455
|
updateState({ loadingType: Global.LoadingTypes.gettingLocation });
|
|
297
|
-
|
|
456
|
+
|
|
457
|
+
let location;
|
|
458
|
+
try {
|
|
459
|
+
// Use combined location with WiFi if available, otherwise just GPS
|
|
460
|
+
if (useWiFiFingerprinting && wifiFingerprint.length > 0) {
|
|
461
|
+
location = await getLocationWithWifi({ getCurrentLocation });
|
|
462
|
+
} else {
|
|
463
|
+
location = await getCurrentLocation();
|
|
464
|
+
}
|
|
465
|
+
} catch (locationError) {
|
|
466
|
+
console.error('Location error:', locationError);
|
|
467
|
+
handleProcessError("Failed to get location. Please try again.");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Validate location object
|
|
472
|
+
if (!location || typeof location !== 'object') {
|
|
473
|
+
handleProcessError("Invalid location data received.");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Validate location coordinates
|
|
478
|
+
if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number' ||
|
|
479
|
+
isNaN(location.latitude) || isNaN(location.longitude)) {
|
|
480
|
+
handleProcessError("Invalid GPS coordinates received.");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
298
483
|
|
|
299
484
|
// Validate location accuracy
|
|
300
485
|
if (location.accuracy > 50) {
|
|
301
486
|
notifyMessage(`Location accuracy is ${location.accuracy.toFixed(1)}m. For best results, move to an open area.`, 'warning');
|
|
302
487
|
}
|
|
303
488
|
|
|
304
|
-
//
|
|
489
|
+
// 6. Perform SECOND WiFi scan to compare with reference
|
|
490
|
+
if (useWiFiFingerprinting && Platform.OS === 'android' && state.wifiReferenceScan) {
|
|
491
|
+
try {
|
|
492
|
+
const wifiResult = await performWiFiScan();
|
|
493
|
+
wifiFingerprint = wifiResult.scan;
|
|
494
|
+
wifiMatchScore = wifiResult.score;
|
|
495
|
+
isWiFiMatch = wifiResult.isMatch;
|
|
496
|
+
wifiMatchDetails = wifiResult.matchDetails;
|
|
497
|
+
} catch (wifiError) {
|
|
498
|
+
console.warn('Second WiFi scan failed:', wifiError);
|
|
499
|
+
// Continue without WiFi match
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 7. Enhanced BLE scanning (optional)
|
|
305
504
|
updateState({ loadingType: Global.LoadingTypes.bleScan });
|
|
306
|
-
startBluetoothScan();
|
|
307
505
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
for (let i = 0; i < 3; i++) {
|
|
311
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
312
|
-
bleSamples.push([...nearbyDevices]);
|
|
313
|
-
}
|
|
506
|
+
let consistentDevices = [];
|
|
507
|
+
let isBLENearby = false;
|
|
314
508
|
|
|
315
|
-
|
|
509
|
+
try {
|
|
510
|
+
startBluetoothScan();
|
|
316
511
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
512
|
+
// Collect BLE data with multiple samples
|
|
513
|
+
const bleSamples = [];
|
|
514
|
+
for (let i = 0; i < 3; i++) {
|
|
515
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
516
|
+
bleSamples.push([...nearbyDevices]);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
consistentDevices = findConsistentBLEDevices(bleSamples);
|
|
520
|
+
isBLENearby = consistentDevices.length > 0 && consistentDevices.some(d => d.avgDistance <= 5);
|
|
521
|
+
} catch (bleError) {
|
|
522
|
+
console.warn("BLE scanning failed:", bleError);
|
|
523
|
+
} finally {
|
|
524
|
+
stopBluetoothScan();
|
|
525
|
+
}
|
|
320
526
|
|
|
321
|
-
//
|
|
527
|
+
// 8. Calculate distance using safe calculation
|
|
322
528
|
updateState({ loadingType: Global.LoadingTypes.calculateDistance });
|
|
323
529
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
location.latitude, location.longitude
|
|
530
|
+
const distance = calculateSafeDistance(
|
|
531
|
+
{ latitude: qrLat, longitude: qrLng },
|
|
532
|
+
{ latitude: location.latitude, longitude: location.longitude }
|
|
328
533
|
);
|
|
329
534
|
|
|
535
|
+
// Validate distance calculation
|
|
536
|
+
if (distance === Infinity || isNaN(distance)) {
|
|
537
|
+
handleProcessError("Failed to calculate distance. Please try again.");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
330
541
|
const distanceWithinThreshold = distance <= MaxDistanceMeters;
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
542
|
+
|
|
543
|
+
// Enhanced verification criteria
|
|
544
|
+
let verificationPassed = distanceWithinThreshold && qrDepKey === depkey;
|
|
545
|
+
let verificationMethod = "GPS+Key";
|
|
546
|
+
|
|
547
|
+
// Add WiFi fingerprint verification if enabled and we have a reference
|
|
548
|
+
if (useWiFiFingerprinting && state.wifiReferenceScan && state.wifiReferenceScan.length > 0) {
|
|
549
|
+
verificationMethod = isWiFiMatch ? "GPS+WiFi+Key" : "GPS+Key";
|
|
550
|
+
|
|
551
|
+
if (isWiFiMatch) {
|
|
552
|
+
// Strong WiFi verification achieved
|
|
553
|
+
} else if (wifiFingerprint.length > 0) {
|
|
554
|
+
notifyMessage(`WiFi environment changed (score: ${wifiMatchScore.toFixed(1)}). Using GPS verification.`, "warning");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (verificationPassed) {
|
|
334
559
|
const locationDetails = {
|
|
335
560
|
qrLocation: {
|
|
336
561
|
latitude: qrLat,
|
|
@@ -342,13 +567,22 @@ const BiometricModal = forwardRef(({
|
|
|
342
567
|
longitude: location.longitude,
|
|
343
568
|
altitude: location.altitude || 0,
|
|
344
569
|
accuracy: location.accuracy,
|
|
345
|
-
speed: location.speed,
|
|
346
|
-
heading: location.heading,
|
|
347
|
-
timestamp: location.timestamp
|
|
570
|
+
speed: location.speed || 0,
|
|
571
|
+
heading: location.heading || 0,
|
|
572
|
+
timestamp: location.timestamp || Date.now()
|
|
573
|
+
},
|
|
574
|
+
wifiFingerprint: {
|
|
575
|
+
accessPoints: wifiFingerprint,
|
|
576
|
+
matchScore: wifiMatchScore,
|
|
577
|
+
isMatch: isWiFiMatch,
|
|
578
|
+
enabled: useWiFiFingerprinting,
|
|
579
|
+
matchDetails: wifiMatchDetails,
|
|
580
|
+
referenceScanTimestamp: state.wifiReferenceScan?.[0]?.timestamp || Date.now()
|
|
348
581
|
},
|
|
349
582
|
distanceMeters: distance,
|
|
350
583
|
accuracyBuffer: MaxDistanceMeters,
|
|
351
584
|
verified: true,
|
|
585
|
+
verificationMethod: verificationMethod,
|
|
352
586
|
verifiedAt: new Date().toISOString(),
|
|
353
587
|
bleDevicesDetected: consistentDevices,
|
|
354
588
|
locationMethod: location.provider || 'unknown',
|
|
@@ -365,93 +599,52 @@ const BiometricModal = forwardRef(({
|
|
|
365
599
|
loadingType: Global.LoadingTypes.none,
|
|
366
600
|
});
|
|
367
601
|
|
|
368
|
-
|
|
602
|
+
let successMessage = `Location verified! Distance: ${distance.toFixed(1)}m (±${Math.max(location.accuracy || 0, qrAccuracy).toFixed(1)}m)`;
|
|
603
|
+
if (useWiFiFingerprinting && isWiFiMatch) {
|
|
604
|
+
successMessage += `, Strong WiFi match: ${wifiMatchScore.toFixed(1)}`;
|
|
605
|
+
} else if (useWiFiFingerprinting && wifiFingerprint.length > 0) {
|
|
606
|
+
successMessage += `, WiFi environment: ${wifiFingerprint.length} APs detected`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
notifyMessage(successMessage, "success");
|
|
369
610
|
setTimeout(() => startFaceRecognition(), 1200);
|
|
370
611
|
} else {
|
|
371
|
-
let errorMsg = `
|
|
372
|
-
if (!isBLENearby) errorMsg += " (No nearby BLE devices)";
|
|
612
|
+
let errorMsg = `Verification failed: ${distance.toFixed(1)}m away`;
|
|
373
613
|
if (qrDepKey !== depkey) errorMsg += " (Key mismatch)";
|
|
614
|
+
if (useWiFiFingerprinting && wifiFingerprint.length > 0) {
|
|
615
|
+
errorMsg += ` (WiFi environment: ${wifiFingerprint.length} APs)`;
|
|
616
|
+
}
|
|
374
617
|
handleProcessError(errorMsg);
|
|
375
618
|
}
|
|
376
619
|
} catch (error) {
|
|
377
620
|
console.error("Enhanced verification failed:", error);
|
|
378
|
-
handleProcessError("Unable to verify location or
|
|
621
|
+
handleProcessError("Unable to verify location or proximity.", error);
|
|
379
622
|
}
|
|
380
623
|
},
|
|
381
624
|
[
|
|
382
625
|
validateApiUrl,
|
|
383
626
|
updateState,
|
|
384
627
|
requestLocationPermission,
|
|
628
|
+
requestBluetoothPermission,
|
|
385
629
|
getCurrentLocation,
|
|
630
|
+
getLocationWithWifi,
|
|
386
631
|
startBluetoothScan,
|
|
387
632
|
stopBluetoothScan,
|
|
388
633
|
nearbyDevices,
|
|
634
|
+
findConsistentBLEDevices,
|
|
389
635
|
notifyMessage,
|
|
390
636
|
handleProcessError,
|
|
391
637
|
startFaceRecognition,
|
|
392
638
|
depkey,
|
|
393
639
|
MaxDistanceMeters,
|
|
640
|
+
calculateSafeDistance,
|
|
641
|
+
useWiFiFingerprinting,
|
|
642
|
+
performWiFiScan,
|
|
643
|
+
state.wifiReferenceScan,
|
|
394
644
|
]
|
|
395
645
|
);
|
|
396
646
|
|
|
397
|
-
|
|
398
|
-
* Find consistent BLE devices across multiple samples
|
|
399
|
-
*/
|
|
400
|
-
const findConsistentBLEDevices = (samples) => {
|
|
401
|
-
const deviceMap = new Map();
|
|
402
|
-
|
|
403
|
-
samples.forEach((sample, sampleIndex) => {
|
|
404
|
-
sample.forEach(device => {
|
|
405
|
-
if (!deviceMap.has(device.id)) {
|
|
406
|
-
deviceMap.set(device.id, {
|
|
407
|
-
...device,
|
|
408
|
-
distances: [],
|
|
409
|
-
samples: 0
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
const entry = deviceMap.get(device.id);
|
|
413
|
-
entry.distances.push(parseFloat(device.distance));
|
|
414
|
-
entry.samples++;
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Filter for devices seen in at least 2 samples
|
|
419
|
-
return Array.from(deviceMap.values())
|
|
420
|
-
.filter(device => device.samples >= 2)
|
|
421
|
-
.map(device => ({
|
|
422
|
-
...device,
|
|
423
|
-
avgDistance: device.distances.reduce((a, b) => a + b, 0) / device.distances.length,
|
|
424
|
-
stdDev: calculateStdDev(device.distances)
|
|
425
|
-
}))
|
|
426
|
-
.filter(device => device.stdDev < 2); // Filter out inconsistent readings
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Calculate standard deviation
|
|
431
|
-
*/
|
|
432
|
-
const calculateStdDev = (array) => {
|
|
433
|
-
const n = array.length;
|
|
434
|
-
const mean = array.reduce((a, b) => a + b) / n;
|
|
435
|
-
return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Calculate enhanced distance with altitude
|
|
440
|
-
*/
|
|
441
|
-
const calculateEnhancedDistance = (lat1, lng1, lat2, lng2, alt1 = 0, alt2 = 0) => {
|
|
442
|
-
const R = 6371000; // Earth's radius in meters
|
|
443
|
-
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
444
|
-
const dLng = (lng2 - lng1) * Math.PI / 180;
|
|
445
|
-
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
446
|
-
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
|
447
|
-
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
448
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
449
|
-
const horizontalDistance = R * c;
|
|
450
|
-
const verticalDistance = Math.abs(alt2 - alt1);
|
|
451
|
-
return Math.sqrt(horizontalDistance * horizontalDistance + verticalDistance * verticalDistance);
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
// Face scan upload - SECOND STEP
|
|
647
|
+
// Face scan upload - SECOND STEP with WiFi re-validation
|
|
455
648
|
const uploadFaceScan = useCallback(
|
|
456
649
|
async (selfie) => {
|
|
457
650
|
if (!validateApiUrl()) return;
|
|
@@ -462,6 +655,25 @@ const BiometricModal = forwardRef(({
|
|
|
462
655
|
return;
|
|
463
656
|
}
|
|
464
657
|
|
|
658
|
+
// Optional: Re-validate WiFi fingerprint during face scan
|
|
659
|
+
let wifiRevalidation = null;
|
|
660
|
+
if (useWiFiFingerprinting && Platform.OS === 'android' && state.wifiReferenceScan) {
|
|
661
|
+
const wifiResult = await performWiFiScan();
|
|
662
|
+
if (wifiResult.scan.length > 0) {
|
|
663
|
+
wifiRevalidation = {
|
|
664
|
+
scan: wifiResult.scan,
|
|
665
|
+
score: wifiResult.score,
|
|
666
|
+
isMatch: wifiResult.isMatch,
|
|
667
|
+
referenceScanLength: state.wifiReferenceScan.length,
|
|
668
|
+
timestamp: new Date().toISOString()
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
if (!wifiResult.isMatch) {
|
|
672
|
+
notifyMessage("Warning: WiFi environment changed since QR scan", "warning");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
465
677
|
const currentData = dataRef.current;
|
|
466
678
|
|
|
467
679
|
if (!currentData) {
|
|
@@ -514,9 +726,10 @@ const BiometricModal = forwardRef(({
|
|
|
514
726
|
if (response?.httpstatus === 200) {
|
|
515
727
|
// Combine face recognition response with QR location data
|
|
516
728
|
responseRef.current = {
|
|
517
|
-
...responseRef.current,
|
|
729
|
+
...responseRef.current,
|
|
518
730
|
...response.data?.data || {},
|
|
519
731
|
faceRecognition: response.data?.data || null,
|
|
732
|
+
wifiRevalidation: wifiRevalidation,
|
|
520
733
|
};
|
|
521
734
|
|
|
522
735
|
updateState({
|
|
@@ -562,7 +775,10 @@ const BiometricModal = forwardRef(({
|
|
|
562
775
|
safeCallback,
|
|
563
776
|
handleProcessError,
|
|
564
777
|
state.qrData,
|
|
565
|
-
|
|
778
|
+
state.wifiReferenceScan,
|
|
779
|
+
apiurl,
|
|
780
|
+
useWiFiFingerprinting,
|
|
781
|
+
performWiFiScan,
|
|
566
782
|
]
|
|
567
783
|
);
|
|
568
784
|
|
|
@@ -661,6 +877,9 @@ const BiometricModal = forwardRef(({
|
|
|
661
877
|
<View style={styles.titleContainer}>
|
|
662
878
|
<Text style={styles.title}>Biometric Verification</Text>
|
|
663
879
|
<Text style={styles.subtitle}>{state.currentStep}</Text>
|
|
880
|
+
{useWiFiFingerprinting && Platform.OS === 'android' && (
|
|
881
|
+
<Text style={styles.wifiIndicator}>WiFi Fingerprinting: Active</Text>
|
|
882
|
+
)}
|
|
664
883
|
</View>
|
|
665
884
|
</View>
|
|
666
885
|
)}
|
|
@@ -700,8 +919,7 @@ const BiometricModal = forwardRef(({
|
|
|
700
919
|
</View>
|
|
701
920
|
</Modal>
|
|
702
921
|
);
|
|
703
|
-
}
|
|
704
|
-
);
|
|
922
|
+
});
|
|
705
923
|
|
|
706
924
|
// Wrap with memo after forwardRef
|
|
707
925
|
const MemoizedBiometricModal = React.memo(BiometricModal);
|
|
@@ -734,6 +952,7 @@ const styles = StyleSheet.create({
|
|
|
734
952
|
position: 'absolute',
|
|
735
953
|
bottom: Platform.OS === 'ios' ? 50 : 30,
|
|
736
954
|
left: 0,
|
|
955
|
+
right: 0,
|
|
737
956
|
zIndex: 10,
|
|
738
957
|
},
|
|
739
958
|
headerContainer: {
|
|
@@ -769,6 +988,16 @@ const styles = StyleSheet.create({
|
|
|
769
988
|
textShadowOffset: { width: 1, height: 1 },
|
|
770
989
|
textShadowRadius: 2,
|
|
771
990
|
},
|
|
991
|
+
wifiIndicator: {
|
|
992
|
+
fontSize: 12,
|
|
993
|
+
color: Global.AppTheme.primary || '#4CAF50',
|
|
994
|
+
marginTop: 4,
|
|
995
|
+
fontWeight: '500',
|
|
996
|
+
fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
|
|
997
|
+
textShadowColor: 'rgba(0, 0, 0, 0.2)',
|
|
998
|
+
textShadowOffset: { width: 1, height: 1 },
|
|
999
|
+
textShadowRadius: 1,
|
|
1000
|
+
},
|
|
772
1001
|
closeButton: {
|
|
773
1002
|
position: 'absolute',
|
|
774
1003
|
top: Platform.OS === 'ios' ? 40 : 20,
|