react-native-biometric-verifier 0.0.46 → 0.0.47

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-biometric-verifier",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "A React Native module for biometric verification with face recognition and QR code scanning",
5
5
  "main": "src/index.js",
6
6
  "private": false,
@@ -0,0 +1,235 @@
1
+ import { useCallback, useState, useRef } from 'react';
2
+ import { Platform, PermissionsAndroid } from 'react-native';
3
+ import { BleManager } from 'react-native-ble-plx';
4
+
5
+ const manager = new BleManager();
6
+
7
+ /**
8
+ * Bluetooth Service Hook for device scanning and distance estimation
9
+ */
10
+ export const useBluetoothService = (notifyMessage) => {
11
+ const [nearbyDevices, setNearbyDevices] = useState([]);
12
+ const scanTimeoutRef = useRef(null);
13
+
14
+ /**
15
+ * Request Bluetooth permissions
16
+ */
17
+ const requestBluetoothPermissions = useCallback(async () => {
18
+ console.log('[Permissions] Requesting Bluetooth permissions...');
19
+
20
+ if (Platform.OS === 'android') {
21
+ try {
22
+ let permissions = [];
23
+
24
+ if (Platform.Version >= 31) {
25
+ // Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT
26
+ permissions = [
27
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
28
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
29
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
30
+ ];
31
+ } else {
32
+ // Older Android versions
33
+ permissions = [
34
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
35
+ ];
36
+ }
37
+
38
+ const granted = await PermissionsAndroid.requestMultiple(permissions);
39
+
40
+ const allGranted = Object.values(granted).every(
41
+ status => status === PermissionsAndroid.RESULTS.GRANTED
42
+ );
43
+
44
+ if (!allGranted) {
45
+ notifyMessage?.('Bluetooth permissions are required for device scanning', 'warning');
46
+ }
47
+
48
+ return allGranted;
49
+ } catch (error) {
50
+ console.error('[Permissions] Error:', error);
51
+ notifyMessage?.('Failed to request Bluetooth permissions', 'error');
52
+ return false;
53
+ }
54
+ }
55
+ return true; // iOS handles permissions differently
56
+ }, [notifyMessage]);
57
+
58
+ /**
59
+ * Enhanced distance calculation using multiple methods
60
+ */
61
+ const estimateDistance = useCallback((rssi, txPower = -59) => {
62
+ if (!rssi || rssi >= 0) return -1;
63
+
64
+ // Method 1: Log-distance path loss model (most common)
65
+ const n = 2.0; // Path loss exponent (2 for free space, 2.7-3.5 for indoor)
66
+ const distance1 = Math.pow(10, (txPower - rssi) / (10 * n));
67
+
68
+ // Method 2: Quadratic approximation for short distances
69
+ const distance2 = 0.89976 * Math.pow(Math.abs(rssi), 0.80976) + 0.111;
70
+
71
+ // Average the methods for better accuracy
72
+ const avgDistance = (distance1 + distance2) / 2;
73
+
74
+ return Math.max(0.1, avgDistance); // Minimum 0.1m
75
+ }, []);
76
+
77
+ /**
78
+ * Filter out stale BLE devices
79
+ */
80
+ const filterStaleDevices = useCallback(() => {
81
+ setNearbyDevices(prev =>
82
+ prev.filter(device => {
83
+ const isRecent = Date.now() - device.lastSeen < 10000; // 10 seconds
84
+ const hasMultipleReadings = device.count >= 3;
85
+ return isRecent && hasMultipleReadings;
86
+ })
87
+ );
88
+ }, []);
89
+
90
+ /**
91
+ * Stop Bluetooth scan
92
+ */
93
+ const stopBluetoothScan = useCallback(() => {
94
+ console.log('[BLE] Stopping Bluetooth scan...');
95
+ manager.stopDeviceScan();
96
+ if (scanTimeoutRef.current) {
97
+ clearTimeout(scanTimeoutRef.current);
98
+ scanTimeoutRef.current = null;
99
+ }
100
+ }, []);
101
+
102
+ /**
103
+ * Enhanced BLE scanning with filtering and signal processing
104
+ */
105
+ const startBluetoothScan = useCallback(async () => {
106
+ console.log('[BLE] Starting enhanced Bluetooth scan...');
107
+
108
+ const permission = await requestBluetoothPermissions();
109
+ if (!permission) {
110
+ return;
111
+ }
112
+
113
+ // Clear previous devices
114
+ setNearbyDevices([]);
115
+
116
+ // Configure scan for better accuracy
117
+ const scanOptions = {
118
+ allowDuplicates: true,
119
+ scanMode: 2, // SCAN_MODE_LOW_LATENCY for Android
120
+ };
121
+
122
+ manager.startDeviceScan(null, scanOptions, (error, device) => {
123
+ if (error) {
124
+ console.error('[BLE] Scan error:', error);
125
+ stopBluetoothScan();
126
+
127
+ // Provide user-friendly error messages
128
+ let errorMessage = 'Bluetooth scan failed';
129
+ if (error.errorCode === 102) {
130
+ errorMessage = 'Bluetooth is not enabled';
131
+ } else if (error.errorCode === 103) {
132
+ errorMessage = 'Location services required for scanning';
133
+ }
134
+
135
+ notifyMessage?.(errorMessage, 'error');
136
+ return;
137
+ }
138
+
139
+ if (device && device.name && device.rssi) {
140
+ const distance = estimateDistance(device.rssi);
141
+
142
+ // Filter out weak signals and calculate moving average
143
+ if (distance > 0 && distance <= 20) {
144
+ setNearbyDevices(prev => {
145
+ const existingIndex = prev.findIndex(d => d.id === device.id);
146
+
147
+ if (existingIndex >= 0) {
148
+ // Update existing device with moving average
149
+ const existing = prev[existingIndex];
150
+ const avgDistance = (parseFloat(existing.distance) + distance) / 2;
151
+
152
+ const updated = [...prev];
153
+ updated[existingIndex] = {
154
+ ...existing,
155
+ rssi: device.rssi,
156
+ distance: avgDistance.toFixed(2),
157
+ lastSeen: Date.now(),
158
+ count: existing.count + 1,
159
+ txPowerLevel: device.txPowerLevel || existing.txPowerLevel,
160
+ };
161
+ return updated;
162
+ } else {
163
+ // Add new device
164
+ const newDevice = {
165
+ id: device.id,
166
+ name: device.name || 'Unknown Device',
167
+ rssi: device.rssi,
168
+ distance: distance.toFixed(2),
169
+ lastSeen: Date.now(),
170
+ count: 1,
171
+ manufacturerData: device.manufacturerData,
172
+ serviceUUIDs: device.serviceUUIDs,
173
+ txPowerLevel: device.txPowerLevel,
174
+ isConnectable: device.isConnectable,
175
+ };
176
+ return [...prev, newDevice];
177
+ }
178
+ });
179
+ }
180
+ }
181
+ });
182
+
183
+ // Stop after optimized duration
184
+ scanTimeoutRef.current = setTimeout(() => {
185
+ stopBluetoothScan();
186
+ filterStaleDevices();
187
+
188
+ if (nearbyDevices.length === 0) {
189
+ notifyMessage?.('No nearby Bluetooth devices found', 'info');
190
+ } else {
191
+ console.log(`[BLE] Found ${nearbyDevices.length} nearby devices`);
192
+ }
193
+ }, 10000);
194
+
195
+ notifyMessage?.('Scanning for nearby Bluetooth devices...', 'info');
196
+ }, [notifyMessage, requestBluetoothPermissions, stopBluetoothScan, filterStaleDevices, estimateDistance]);
197
+
198
+ /**
199
+ * Get device details by ID
200
+ */
201
+ const getDeviceDetails = useCallback(async (deviceId) => {
202
+ try {
203
+ const device = await manager.connectToDevice(deviceId);
204
+ await device.discoverAllServicesAndCharacteristics();
205
+ const services = await device.services();
206
+
207
+ return {
208
+ ...device,
209
+ services,
210
+ isConnected: true,
211
+ };
212
+ } catch (error) {
213
+ console.error('[BLE] Error getting device details:', error);
214
+ return null;
215
+ }
216
+ }, []);
217
+
218
+ /**
219
+ * Clear all scanned devices
220
+ */
221
+ const clearDevices = useCallback(() => {
222
+ setNearbyDevices([]);
223
+ }, []);
224
+
225
+ return {
226
+ requestBluetoothPermission: requestBluetoothPermissions,
227
+ startBluetoothScan,
228
+ stopBluetoothScan,
229
+ nearbyDevices,
230
+ filterStaleDevices,
231
+ clearDevices,
232
+ getDeviceDetails,
233
+ estimateDistance,
234
+ };
235
+ };
@@ -1,32 +1,24 @@
1
- // Enhanced location service
2
- import { useCallback, useState, useRef } from 'react';
1
+ import { useCallback, useRef } from 'react';
3
2
  import { Platform, PermissionsAndroid } from 'react-native';
4
3
  import Geolocation from 'react-native-geolocation-service';
5
- import { BleManager } from 'react-native-ble-plx';
6
-
7
- const manager = new BleManager();
8
4
 
9
5
  /**
10
6
  * Enhanced Geolocation Hook with better accuracy
11
7
  */
12
8
  export const useGeolocation = (notifyMessage) => {
13
- const [nearbyDevices, setNearbyDevices] = useState([]);
14
- const scanTimeoutRef = useRef(null);
15
9
  const locationWatchId = useRef(null);
16
10
 
17
11
  /**
18
- * Request high-accuracy permissions
12
+ * Request high-accuracy location permissions
19
13
  */
20
- const requestPermissions = useCallback(async () => {
21
- console.log('[Permissions] Requesting enhanced permissions...');
14
+ const requestLocationPermissions = useCallback(async () => {
15
+ console.log('[Permissions] Requesting enhanced location permissions...');
22
16
 
23
17
  if (Platform.OS === 'android') {
24
18
  try {
25
19
  const granted = await PermissionsAndroid.requestMultiple([
26
20
  PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
27
21
  PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
28
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
29
- PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
30
22
  ]);
31
23
 
32
24
  // Check Android version for background location
@@ -36,16 +28,23 @@ export const useGeolocation = (notifyMessage) => {
36
28
  );
37
29
  }
38
30
 
39
- return Object.values(granted).every(
31
+ const allGranted = Object.values(granted).every(
40
32
  status => status === PermissionsAndroid.RESULTS.GRANTED
41
33
  );
34
+
35
+ if (!allGranted) {
36
+ notifyMessage?.('Location permissions are required for accurate positioning', 'warning');
37
+ }
38
+
39
+ return allGranted;
42
40
  } catch (error) {
43
41
  console.error('[Permissions] Error:', error);
42
+ notifyMessage?.('Failed to request location permissions', 'error');
44
43
  return false;
45
44
  }
46
45
  }
47
46
  return true; // iOS handles permissions differently
48
- }, []);
47
+ }, [notifyMessage]);
49
48
 
50
49
  /**
51
50
  * Get high-accuracy location with multiple attempts
@@ -56,8 +55,6 @@ export const useGeolocation = (notifyMessage) => {
56
55
  console.log('[Location] Fetching enhanced location...');
57
56
 
58
57
  let bestLocation = null;
59
- let attempts = 0;
60
- const maxAttempts = 3;
61
58
 
62
59
  const locationOptions = {
63
60
  enableHighAccuracy: true,
@@ -138,130 +135,39 @@ export const useGeolocation = (notifyMessage) => {
138
135
  resolve(bestLocation);
139
136
  } catch (error) {
140
137
  console.error('[Location] Error:', error);
141
- reject(error);
138
+
139
+ // Provide user-friendly error messages
140
+ let errorMessage = 'Failed to get location';
141
+ switch (error.code) {
142
+ case 1:
143
+ errorMessage = 'Location permission denied';
144
+ break;
145
+ case 2:
146
+ errorMessage = 'Location unavailable';
147
+ break;
148
+ case 3:
149
+ errorMessage = 'Location request timed out';
150
+ break;
151
+ }
152
+
153
+ notifyMessage?.(errorMessage, 'error');
154
+ reject(new Error(errorMessage));
142
155
  }
143
156
  }),
144
157
  [notifyMessage]
145
158
  );
146
159
 
147
160
  /**
148
- * Stop Bluetooth scan
161
+ * Stop location watching
149
162
  */
150
- const stopBluetoothScan = useCallback(() => {
151
- console.log('[BLE] Stopping Bluetooth scan...');
152
- manager.stopDeviceScan();
153
- if (scanTimeoutRef.current) {
154
- clearTimeout(scanTimeoutRef.current);
155
- scanTimeoutRef.current = null;
163
+ const stopLocationWatching = useCallback(() => {
164
+ if (locationWatchId.current !== null) {
165
+ console.log('[Location] Stopping location watch...');
166
+ Geolocation.clearWatch(locationWatchId.current);
167
+ locationWatchId.current = null;
156
168
  }
157
169
  }, []);
158
170
 
159
- /**
160
- * Enhanced BLE scanning with filtering
161
- */
162
- const startBluetoothScan = useCallback(async () => {
163
- console.log('[BLE] Starting enhanced Bluetooth scan...');
164
- const permission = await requestPermissions();
165
- if (!permission) {
166
- notifyMessage?.('Permissions required for Bluetooth scanning', 'error');
167
- return;
168
- }
169
-
170
- setNearbyDevices([]);
171
-
172
- // Configure scan for better accuracy
173
- const scanOptions = {
174
- allowDuplicates: true,
175
- scanMode: 2, // SCAN_MODE_LOW_LATENCY for Android
176
- };
177
-
178
- manager.startDeviceScan(null, scanOptions, (error, device) => {
179
- if (error) {
180
- console.error('[BLE] Scan error:', error);
181
- stopBluetoothScan();
182
- return;
183
- }
184
-
185
- if (device && device.name && device.rssi) {
186
- const distance = estimateDistance(device.rssi);
187
-
188
- // Filter out weak signals and calculate moving average
189
- if (distance > 0 && distance <= 20) {
190
- setNearbyDevices(prev => {
191
- const existingIndex = prev.findIndex(d => d.id === device.id);
192
-
193
- if (existingIndex >= 0) {
194
- // Update existing device with moving average
195
- const existing = prev[existingIndex];
196
- const avgDistance = (parseFloat(existing.distance) + distance) / 2;
197
-
198
- const updated = [...prev];
199
- updated[existingIndex] = {
200
- ...existing,
201
- rssi: device.rssi,
202
- distance: avgDistance.toFixed(2),
203
- lastSeen: Date.now(),
204
- count: existing.count + 1,
205
- };
206
- return updated;
207
- } else {
208
- // Add new device
209
- const newDevice = {
210
- id: device.id,
211
- name: device.name,
212
- rssi: device.rssi,
213
- distance: distance.toFixed(2),
214
- lastSeen: Date.now(),
215
- count: 1,
216
- manufacturerData: device.manufacturerData,
217
- serviceUUIDs: device.serviceUUIDs,
218
- };
219
- return [...prev, newDevice];
220
- }
221
- });
222
- }
223
- }
224
- });
225
-
226
- // Stop after optimized duration
227
- scanTimeoutRef.current = setTimeout(() => {
228
- stopBluetoothScan();
229
- filterStaleDevices();
230
- }, 10000);
231
- }, [notifyMessage, requestPermissions, stopBluetoothScan, filterStaleDevices]);
232
-
233
- /**
234
- * Filter out stale BLE devices
235
- */
236
- const filterStaleDevices = useCallback(() => {
237
- setNearbyDevices(prev =>
238
- prev.filter(device => {
239
- const isRecent = Date.now() - device.lastSeen < 10000; // 10 seconds
240
- const hasMultipleReadings = device.count >= 3;
241
- return isRecent && hasMultipleReadings;
242
- })
243
- );
244
- }, []);
245
-
246
- /**
247
- * Enhanced distance calculation using multiple methods
248
- */
249
- const estimateDistance = useCallback((rssi, txPower = -59) => {
250
- if (!rssi || rssi >= 0) return -1;
251
-
252
- // Method 1: Log-distance path loss model (most common)
253
- const n = 2.0; // Path loss exponent (2 for free space, 2.7-3.5 for indoor)
254
- const distance1 = Math.pow(10, (txPower - rssi) / (10 * n));
255
-
256
- // Method 2: Quadratic approximation for short distances
257
- const distance2 = 0.89976 * Math.pow(Math.abs(rssi), 0.80976) + 0.111;
258
-
259
- // Average the methods for better accuracy
260
- const avgDistance = (distance1 + distance2) / 2;
261
-
262
- return Math.max(0.1, avgDistance); // Minimum 0.1m
263
- }, []);
264
-
265
171
  /**
266
172
  * Calculate distance between coordinates using Haversine formula with altitude
267
173
  */
@@ -288,12 +194,9 @@ export const useGeolocation = (notifyMessage) => {
288
194
  }, []);
289
195
 
290
196
  return {
291
- requestLocationPermission: requestPermissions,
197
+ requestLocationPermission: requestLocationPermissions,
292
198
  getCurrentLocation,
293
- startBluetoothScan,
294
- stopBluetoothScan,
295
- nearbyDevices,
199
+ stopLocationWatching,
296
200
  calculateEnhancedDistance,
297
- filterStaleDevices,
298
201
  };
299
202
  };
package/src/index.js CHANGED
@@ -17,15 +17,16 @@ 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 { useBluetoothService } from "./hooks/useBluetoothService";
23
25
  import { useImageProcessing } from "./hooks/useImageProcessing";
24
26
  import { useNotifyMessage } from "./hooks/useNotifyMessage";
25
27
  import { useSafeCallback } from "./hooks/useSafeCallback";
26
28
 
27
29
  // Utils
28
- import { getDistanceInMeters } from "./utils/distanceCalculator";
29
30
  import { Global } from "./utils/Global";
30
31
  import networkServiceCall from "./utils/NetworkServiceCall";
31
32
  import { getLoaderGif } from "./utils/getLoaderGif";
@@ -56,13 +57,23 @@ const BiometricModal = forwardRef(({
56
57
  // Custom hooks - Initialize notification hook first so notifyMessage is available for other hooks
57
58
  const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
58
59
  const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
60
+
61
+ // Separate hooks for geolocation and bluetooth
59
62
  const {
60
63
  requestLocationPermission,
61
64
  getCurrentLocation,
65
+ stopLocationWatching,
66
+ calculateEnhancedDistance,
67
+ } = useGeolocation(notifyMessage);
68
+
69
+ const {
70
+ requestBluetoothPermission,
62
71
  startBluetoothScan,
63
72
  stopBluetoothScan,
64
73
  nearbyDevices,
65
- } = useGeolocation(notifyMessage);
74
+ clearDevices,
75
+ } = useBluetoothService(notifyMessage);
76
+
66
77
  const { convertImageToBase64 } = useImageProcessing();
67
78
  const safeCallback = useSafeCallback(callback, notifyMessage);
68
79
 
@@ -84,6 +95,8 @@ const BiometricModal = forwardRef(({
84
95
  const responseRef = useRef(null);
85
96
  const processedRef = useRef(false);
86
97
  const resetTimeoutRef = useRef(null);
98
+ const bleScanTimeoutRef = useRef(null);
99
+
87
100
  // Animation values
88
101
  const iconScaleAnim = useRef(new Animated.Value(1)).current;
89
102
  const iconOpacityAnim = useRef(new Animated.Value(0)).current;
@@ -104,10 +117,17 @@ const BiometricModal = forwardRef(({
104
117
  if (resetTimeoutRef.current) {
105
118
  clearTimeout(resetTimeoutRef.current);
106
119
  }
107
-
120
+
121
+ if (bleScanTimeoutRef.current) {
122
+ clearTimeout(bleScanTimeoutRef.current);
123
+ }
124
+
125
+ stopBluetoothScan();
126
+ stopLocationWatching();
127
+ clearDevices();
108
128
  clearNotification();
109
129
  };
110
- }, []);
130
+ }, [stopBluetoothScan, stopLocationWatching, clearDevices, clearNotification]);
111
131
 
112
132
  // Update dataRef when data changes
113
133
  useEffect(() => {
@@ -189,13 +209,16 @@ const BiometricModal = forwardRef(({
189
209
  setModalVisible(false);
190
210
  processedRef.current = false;
191
211
  resetCountdown();
212
+ stopBluetoothScan();
213
+ stopLocationWatching();
214
+ clearDevices();
192
215
  clearNotification();
193
216
 
194
217
  if (resetTimeoutRef.current) {
195
218
  clearTimeout(resetTimeoutRef.current);
196
219
  resetTimeoutRef.current = null;
197
220
  }
198
- }, [resetCountdown, clearNotification, onclose]);
221
+ }, [resetCountdown, stopBluetoothScan, stopLocationWatching, clearDevices, clearNotification, onclose]);
199
222
 
200
223
  // Error handler
201
224
  const handleProcessError = useCallback(
@@ -241,6 +264,47 @@ const BiometricModal = forwardRef(({
241
264
  return true;
242
265
  }, [apiurl, handleProcessError]);
243
266
 
267
+ /**
268
+ * Find consistent BLE devices across multiple samples
269
+ */
270
+ const findConsistentBLEDevices = useCallback((samples) => {
271
+ const deviceMap = new Map();
272
+
273
+ samples.forEach((sample, sampleIndex) => {
274
+ sample.forEach(device => {
275
+ if (!deviceMap.has(device.id)) {
276
+ deviceMap.set(device.id, {
277
+ ...device,
278
+ distances: [],
279
+ samples: 0
280
+ });
281
+ }
282
+ const entry = deviceMap.get(device.id);
283
+ entry.distances.push(parseFloat(device.distance));
284
+ entry.samples++;
285
+ });
286
+ });
287
+
288
+ // Filter for devices seen in at least 2 samples
289
+ return Array.from(deviceMap.values())
290
+ .filter(device => device.samples >= 2)
291
+ .map(device => ({
292
+ ...device,
293
+ avgDistance: device.distances.reduce((a, b) => a + b, 0) / device.distances.length,
294
+ stdDev: calculateStdDev(device.distances)
295
+ }))
296
+ .filter(device => device.stdDev < 2); // Filter out inconsistent readings
297
+ }, []);
298
+
299
+ /**
300
+ * Calculate standard deviation
301
+ */
302
+ const calculateStdDev = (array) => {
303
+ const n = array.length;
304
+ const mean = array.reduce((a, b) => a + b) / n;
305
+ return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
306
+ };
307
+
244
308
  // QR code processing - FIRST STEP
245
309
  // Enhanced QR processing
246
310
  const handleQRScanned = useCallback(
@@ -256,12 +320,18 @@ const BiometricModal = forwardRef(({
256
320
  try {
257
321
  // 1. Request permissions
258
322
  updateState({ loadingType: Global.LoadingTypes.locationPermission });
259
- const hasPermission = await requestLocationPermission();
260
- if (!hasPermission) {
323
+ const hasLocationPermission = await requestLocationPermission();
324
+ if (!hasLocationPermission) {
261
325
  handleProcessError("Location permission not granted.");
262
326
  return;
263
327
  }
264
328
 
329
+ const hasBluetoothPermission = await requestBluetoothPermission();
330
+ if (!hasBluetoothPermission) {
331
+ notifyMessage("Bluetooth scanning may not work", "warning");
332
+ // Continue anyway, Bluetooth is optional
333
+ }
334
+
265
335
  // 2. Parse QR data with validation
266
336
  const qrString = typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
267
337
  if (!qrString || typeof qrString !== "string") {
@@ -303,25 +373,34 @@ const BiometricModal = forwardRef(({
303
373
 
304
374
  // 4. Enhanced BLE scanning (optional - can be removed if not needed)
305
375
  updateState({ loadingType: Global.LoadingTypes.bleScan });
306
- startBluetoothScan();
307
-
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]);
376
+
377
+ // Start BLE scan and collect samples
378
+ let consistentDevices = [];
379
+ let isBLENearby = false;
380
+
381
+ try {
382
+ startBluetoothScan();
383
+
384
+ // Collect BLE data with multiple samples
385
+ const bleSamples = [];
386
+ for (let i = 0; i < 3; i++) {
387
+ await new Promise(resolve => setTimeout(resolve, 2000));
388
+ bleSamples.push([...nearbyDevices]);
389
+ }
390
+
391
+ consistentDevices = findConsistentBLEDevices(bleSamples);
392
+ isBLENearby = consistentDevices.length > 0 && consistentDevices.some(d => d.avgDistance <= 5);
393
+ } catch (bleError) {
394
+ console.warn("BLE scanning failed:", bleError);
395
+ // Continue without BLE data
396
+ } finally {
397
+ stopBluetoothScan();
313
398
  }
314
399
 
315
- stopBluetoothScan();
316
-
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);
320
-
321
400
  // 5. Calculate distance (no altitude in your QR code)
322
401
  updateState({ loadingType: Global.LoadingTypes.calculateDistance });
323
402
 
324
- // Simple 2D distance calculation (since no altitude in QR)
403
+ // Use the enhanced distance calculation from geolocation hook
325
404
  const distance = calculateEnhancedDistance(
326
405
  qrLat, qrLng,
327
406
  location.latitude, location.longitude
@@ -329,7 +408,8 @@ const BiometricModal = forwardRef(({
329
408
 
330
409
  const distanceWithinThreshold = distance <= MaxDistanceMeters;
331
410
  console.log('distanceWithinThreshold', distance, MaxDistanceMeters)
332
- // Verify: distance within threshold AND BLE devices nearby AND depkey matches
411
+
412
+ // Verify: distance within threshold AND depkey matches (BLE is optional)
333
413
  if (distanceWithinThreshold && qrDepKey === depkey) {
334
414
  const locationDetails = {
335
415
  qrLocation: {
@@ -369,7 +449,6 @@ const BiometricModal = forwardRef(({
369
449
  setTimeout(() => startFaceRecognition(), 1200);
370
450
  } else {
371
451
  let errorMsg = `Location mismatch: ${distance.toFixed(1)}m away`;
372
- if (!isBLENearby) errorMsg += " (No nearby BLE devices)";
373
452
  if (qrDepKey !== depkey) errorMsg += " (Key mismatch)";
374
453
  handleProcessError(errorMsg);
375
454
  }
@@ -382,75 +461,21 @@ const BiometricModal = forwardRef(({
382
461
  validateApiUrl,
383
462
  updateState,
384
463
  requestLocationPermission,
464
+ requestBluetoothPermission,
385
465
  getCurrentLocation,
386
466
  startBluetoothScan,
387
467
  stopBluetoothScan,
388
468
  nearbyDevices,
469
+ findConsistentBLEDevices,
389
470
  notifyMessage,
390
471
  handleProcessError,
391
472
  startFaceRecognition,
392
473
  depkey,
393
474
  MaxDistanceMeters,
475
+ calculateEnhancedDistance,
394
476
  ]
395
477
  );
396
478
 
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
479
  // Face scan upload - SECOND STEP
455
480
  const uploadFaceScan = useCallback(
456
481
  async (selfie) => {
@@ -700,8 +725,7 @@ const BiometricModal = forwardRef(({
700
725
  </View>
701
726
  </Modal>
702
727
  );
703
- }
704
- );
728
+ });
705
729
 
706
730
  // Wrap with memo after forwardRef
707
731
  const MemoizedBiometricModal = React.memo(BiometricModal);