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/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 = 50,
54
- duration = 100
55
+ MaxDistanceMeters = 30,
56
+ duration = 100,
57
+ useWiFiFingerprinting = true,
55
58
  }, ref) => {
56
- // Custom hooks - Initialize notification hook first so notifyMessage is available for other hooks
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
- } = useGeolocation(notifyMessage);
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"); // Start with back camera for QR scan
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, // Start with QR scan animation
78
- qrData: null, // Store QR data for later use in face recognition
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
- // QR code processing - FIRST STEP
245
- // Enhanced QR processing
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 hasPermission = await requestLocationPermission();
260
- if (!hasPermission) {
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 in format: latitude,longitude,depkey
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]; // This is your 1234 from the QR code
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
- // 3. Get high-accuracy location
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
- const location = await getCurrentLocation();
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
- // 4. Enhanced BLE scanning (optional - can be removed if not needed)
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
- // Collect BLE data with multiple samples
309
- const bleSamples = [];
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
- stopBluetoothScan();
509
+ try {
510
+ startBluetoothScan();
316
511
 
317
- // Process BLE data to find consistent nearby devices
318
- const consistentDevices = findConsistentBLEDevices(bleSamples);
319
- const isBLENearby = consistentDevices.length > 0 && consistentDevices.some(d => d.avgDistance <= 5);
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
- // 5. Calculate distance (no altitude in your QR code)
527
+ // 8. Calculate distance using safe calculation
322
528
  updateState({ loadingType: Global.LoadingTypes.calculateDistance });
323
529
 
324
- // Simple 2D distance calculation (since no altitude in QR)
325
- const distance = calculateEnhancedDistance(
326
- qrLat, qrLng,
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
- console.log('distanceWithinThreshold', distance, MaxDistanceMeters)
332
- // Verify: distance within threshold AND BLE devices nearby AND depkey matches
333
- if (distanceWithinThreshold && qrDepKey === depkey) {
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
- notifyMessage(`Location verified! Distance: ${distance.toFixed(1)}m (±${Math.max(location.accuracy, qrAccuracy).toFixed(1)}m)`, "success");
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 = `Location mismatch: ${distance.toFixed(1)}m away`;
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 BLE proximity.", error);
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, // Contains location data from QR scan
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
- apiurl
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,