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 +1 -1
- package/src/hooks/useBluetoothService.js +235 -0
- package/src/hooks/useGeolocation.js +38 -135
- package/src/index.js +106 -82
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
161
|
+
* Stop location watching
|
|
149
162
|
*/
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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:
|
|
197
|
+
requestLocationPermission: requestLocationPermissions,
|
|
292
198
|
getCurrentLocation,
|
|
293
|
-
|
|
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
|
-
|
|
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
|
|
260
|
-
if (!
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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);
|