react-native-biometric-verifier 0.0.47 → 0.0.49
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 +79 -119
- package/src/hooks/useGeolocation.js +226 -175
- package/src/hooks/useWifiService.js +175 -0
- package/src/index.js +87 -149
package/package.json
CHANGED
|
@@ -11,74 +11,60 @@ export const useBluetoothService = (notifyMessage) => {
|
|
|
11
11
|
const [nearbyDevices, setNearbyDevices] = useState([]);
|
|
12
12
|
const scanTimeoutRef = useRef(null);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/* -------------------------------------------------------------------------- */
|
|
15
|
+
/* BLUETOOTH PERMISSIONS */
|
|
16
|
+
/* -------------------------------------------------------------------------- */
|
|
17
17
|
const requestBluetoothPermissions = useCallback(async () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} else {
|
|
32
|
-
// Older Android versions
|
|
33
|
-
permissions = [
|
|
34
|
-
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
35
|
-
];
|
|
36
|
-
}
|
|
18
|
+
if (Platform.OS !== 'android') return true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let permissions = [];
|
|
22
|
+
if (Platform.Version >= 31) {
|
|
23
|
+
permissions = [
|
|
24
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
|
25
|
+
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
|
26
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
27
|
+
];
|
|
28
|
+
} else {
|
|
29
|
+
permissions = [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
|
|
30
|
+
}
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
const granted = await PermissionsAndroid.requestMultiple(permissions);
|
|
33
|
+
const allGranted = Object.values(granted).every(
|
|
34
|
+
status => status === PermissionsAndroid.RESULTS.GRANTED
|
|
35
|
+
);
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
|
37
|
+
if (!allGranted) {
|
|
38
|
+
notifyMessage?.('Bluetooth permissions are required for device scanning', 'warning');
|
|
53
39
|
}
|
|
40
|
+
|
|
41
|
+
return allGranted;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('[Permissions] Error:', error);
|
|
44
|
+
notifyMessage?.('Failed to request Bluetooth permissions', 'error');
|
|
45
|
+
return false;
|
|
54
46
|
}
|
|
55
|
-
return true; // iOS handles permissions differently
|
|
56
47
|
}, [notifyMessage]);
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
/* -------------------------------------------------------------------------- */
|
|
50
|
+
/* DISTANCE ESTIMATION */
|
|
51
|
+
/* -------------------------------------------------------------------------- */
|
|
61
52
|
const estimateDistance = useCallback((rssi, txPower = -59) => {
|
|
62
53
|
if (!rssi || rssi >= 0) return -1;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const n = 2.0; // Path loss exponent (2 for free space, 2.7-3.5 for indoor)
|
|
54
|
+
|
|
55
|
+
const n = 2.0;
|
|
66
56
|
const distance1 = Math.pow(10, (txPower - rssi) / (10 * n));
|
|
67
|
-
|
|
68
|
-
// Method 2: Quadratic approximation for short distances
|
|
69
57
|
const distance2 = 0.89976 * Math.pow(Math.abs(rssi), 0.80976) + 0.111;
|
|
70
|
-
|
|
71
|
-
// Average the methods for better accuracy
|
|
72
58
|
const avgDistance = (distance1 + distance2) / 2;
|
|
73
|
-
|
|
74
|
-
return Math.max(0.1, avgDistance);
|
|
59
|
+
|
|
60
|
+
return Math.max(0.1, avgDistance);
|
|
75
61
|
}, []);
|
|
76
62
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
/* -------------------------------------------------------------------------- */
|
|
64
|
+
/* FILTER STALE DEVICES */
|
|
65
|
+
/* -------------------------------------------------------------------------- */
|
|
80
66
|
const filterStaleDevices = useCallback(() => {
|
|
81
|
-
setNearbyDevices(prev =>
|
|
67
|
+
setNearbyDevices(prev =>
|
|
82
68
|
prev.filter(device => {
|
|
83
69
|
const isRecent = Date.now() - device.lastSeen < 10000; // 10 seconds
|
|
84
70
|
const hasMultipleReadings = device.count >= 3;
|
|
@@ -87,11 +73,10 @@ export const useBluetoothService = (notifyMessage) => {
|
|
|
87
73
|
);
|
|
88
74
|
}, []);
|
|
89
75
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
/* -------------------------------------------------------------------------- */
|
|
77
|
+
/* STOP SCAN */
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
93
79
|
const stopBluetoothScan = useCallback(() => {
|
|
94
|
-
console.log('[BLE] Stopping Bluetooth scan...');
|
|
95
80
|
manager.stopDeviceScan();
|
|
96
81
|
if (scanTimeoutRef.current) {
|
|
97
82
|
clearTimeout(scanTimeoutRef.current);
|
|
@@ -99,56 +84,43 @@ export const useBluetoothService = (notifyMessage) => {
|
|
|
99
84
|
}
|
|
100
85
|
}, []);
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
/* -------------------------------------------------------------------------- */
|
|
88
|
+
/* START BLE SCAN */
|
|
89
|
+
/* -------------------------------------------------------------------------- */
|
|
105
90
|
const startBluetoothScan = useCallback(async () => {
|
|
106
|
-
console.log('[BLE] Starting enhanced Bluetooth scan...');
|
|
107
|
-
|
|
108
91
|
const permission = await requestBluetoothPermissions();
|
|
109
|
-
if (!permission)
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
92
|
+
if (!permission) return;
|
|
112
93
|
|
|
113
|
-
// Clear previous devices
|
|
114
94
|
setNearbyDevices([]);
|
|
115
|
-
|
|
116
|
-
// Configure scan for better accuracy
|
|
95
|
+
|
|
117
96
|
const scanOptions = {
|
|
118
97
|
allowDuplicates: true,
|
|
119
|
-
scanMode: 2,
|
|
98
|
+
scanMode: 2,
|
|
120
99
|
};
|
|
121
100
|
|
|
122
101
|
manager.startDeviceScan(null, scanOptions, (error, device) => {
|
|
123
102
|
if (error) {
|
|
124
103
|
console.error('[BLE] Scan error:', error);
|
|
125
104
|
stopBluetoothScan();
|
|
126
|
-
|
|
127
|
-
// Provide user-friendly error messages
|
|
105
|
+
|
|
128
106
|
let errorMessage = 'Bluetooth scan failed';
|
|
129
|
-
if (error.errorCode === 102)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
errorMessage = 'Location services required for scanning';
|
|
133
|
-
}
|
|
134
|
-
|
|
107
|
+
if (error.errorCode === 102) errorMessage = 'Bluetooth is not enabled';
|
|
108
|
+
else if (error.errorCode === 103) errorMessage = 'Location services required for scanning';
|
|
109
|
+
|
|
135
110
|
notifyMessage?.(errorMessage, 'error');
|
|
136
111
|
return;
|
|
137
112
|
}
|
|
138
113
|
|
|
139
114
|
if (device && device.name && device.rssi) {
|
|
140
115
|
const distance = estimateDistance(device.rssi);
|
|
141
|
-
|
|
142
|
-
// Filter out weak signals and calculate moving average
|
|
116
|
+
|
|
143
117
|
if (distance > 0 && distance <= 20) {
|
|
144
118
|
setNearbyDevices(prev => {
|
|
145
119
|
const existingIndex = prev.findIndex(d => d.id === device.id);
|
|
146
|
-
|
|
147
120
|
if (existingIndex >= 0) {
|
|
148
|
-
// Update existing device with moving average
|
|
149
121
|
const existing = prev[existingIndex];
|
|
150
122
|
const avgDistance = (parseFloat(existing.distance) + distance) / 2;
|
|
151
|
-
|
|
123
|
+
|
|
152
124
|
const updated = [...prev];
|
|
153
125
|
updated[existingIndex] = {
|
|
154
126
|
...existing,
|
|
@@ -160,64 +132,52 @@ export const useBluetoothService = (notifyMessage) => {
|
|
|
160
132
|
};
|
|
161
133
|
return updated;
|
|
162
134
|
} else {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
135
|
+
return [
|
|
136
|
+
...prev,
|
|
137
|
+
{
|
|
138
|
+
id: device.id,
|
|
139
|
+
name: device.name || 'Unknown Device',
|
|
140
|
+
rssi: device.rssi,
|
|
141
|
+
distance: distance.toFixed(2),
|
|
142
|
+
lastSeen: Date.now(),
|
|
143
|
+
count: 1,
|
|
144
|
+
manufacturerData: device.manufacturerData,
|
|
145
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
146
|
+
txPowerLevel: device.txPowerLevel,
|
|
147
|
+
isConnectable: device.isConnectable,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
177
150
|
}
|
|
178
151
|
});
|
|
179
152
|
}
|
|
180
153
|
}
|
|
181
154
|
});
|
|
182
155
|
|
|
183
|
-
// Stop after optimized duration
|
|
184
156
|
scanTimeoutRef.current = setTimeout(() => {
|
|
185
157
|
stopBluetoothScan();
|
|
186
158
|
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
159
|
}, 10000);
|
|
160
|
+
}, [notifyMessage, requestBluetoothPermissions, stopBluetoothScan, filterStaleDevices, estimateDistance, nearbyDevices]);
|
|
194
161
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get device details by ID
|
|
200
|
-
*/
|
|
162
|
+
/* -------------------------------------------------------------------------- */
|
|
163
|
+
/* DEVICE DETAILS */
|
|
164
|
+
/* -------------------------------------------------------------------------- */
|
|
201
165
|
const getDeviceDetails = useCallback(async (deviceId) => {
|
|
202
166
|
try {
|
|
203
167
|
const device = await manager.connectToDevice(deviceId);
|
|
204
168
|
await device.discoverAllServicesAndCharacteristics();
|
|
205
169
|
const services = await device.services();
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
...device,
|
|
209
|
-
services,
|
|
210
|
-
isConnected: true,
|
|
211
|
-
};
|
|
170
|
+
|
|
171
|
+
return { ...device, services, isConnected: true };
|
|
212
172
|
} catch (error) {
|
|
213
173
|
console.error('[BLE] Error getting device details:', error);
|
|
214
174
|
return null;
|
|
215
175
|
}
|
|
216
176
|
}, []);
|
|
217
177
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
178
|
+
/* -------------------------------------------------------------------------- */
|
|
179
|
+
/* CLEAR DEVICES */
|
|
180
|
+
/* -------------------------------------------------------------------------- */
|
|
221
181
|
const clearDevices = useCallback(() => {
|
|
222
182
|
setNearbyDevices([]);
|
|
223
183
|
}, []);
|
|
@@ -232,4 +192,4 @@ export const useBluetoothService = (notifyMessage) => {
|
|
|
232
192
|
getDeviceDetails,
|
|
233
193
|
estimateDistance,
|
|
234
194
|
};
|
|
235
|
-
};
|
|
195
|
+
};
|
|
@@ -3,200 +3,251 @@ import { Platform, PermissionsAndroid } from 'react-native';
|
|
|
3
3
|
import Geolocation from 'react-native-geolocation-service';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* FAST + ACCURATE Geolocation Hook (Optimized for Verification)
|
|
7
7
|
*/
|
|
8
8
|
export const useGeolocation = (notifyMessage) => {
|
|
9
9
|
const locationWatchId = useRef(null);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (Platform.Version >= 29) {
|
|
26
|
-
await PermissionsAndroid.request(
|
|
27
|
-
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const allGranted = Object.values(granted).every(
|
|
32
|
-
status => status === PermissionsAndroid.RESULTS.GRANTED
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (!allGranted) {
|
|
36
|
-
notifyMessage?.('Location permissions are required for accurate positioning', 'warning');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return allGranted;
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error('[Permissions] Error:', error);
|
|
42
|
-
notifyMessage?.('Failed to request location permissions', 'error');
|
|
43
|
-
return false;
|
|
11
|
+
/* -------------------------------------------------------------------------- */
|
|
12
|
+
/* PERMISSIONS */
|
|
13
|
+
/* -------------------------------------------------------------------------- */
|
|
14
|
+
const requestLocationPermission = useCallback(async () => {
|
|
15
|
+
try {
|
|
16
|
+
if (Platform.OS !== 'android') return true;
|
|
17
|
+
|
|
18
|
+
const permissions = [
|
|
19
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
20
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (Platform.Version >= 31) {
|
|
24
|
+
permissions.push(PermissionsAndroid.PERMISSIONS.NEARBY_WIFI_DEVICES);
|
|
44
25
|
}
|
|
45
|
-
}
|
|
46
|
-
return true; // iOS handles permissions differently
|
|
47
|
-
}, [notifyMessage]);
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
* Get high-accuracy location with multiple attempts
|
|
51
|
-
*/
|
|
52
|
-
const getCurrentLocation = useCallback(
|
|
53
|
-
() =>
|
|
54
|
-
new Promise(async (resolve, reject) => {
|
|
55
|
-
console.log('[Location] Fetching enhanced location...');
|
|
56
|
-
|
|
57
|
-
let bestLocation = null;
|
|
58
|
-
|
|
59
|
-
const locationOptions = {
|
|
60
|
-
enableHighAccuracy: true,
|
|
61
|
-
timeout: 20000,
|
|
62
|
-
maximumAge: 0,
|
|
63
|
-
distanceFilter: 0,
|
|
64
|
-
forceRequestLocation: true,
|
|
65
|
-
showLocationDialog: true,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const watchLocationForAccuracy = () => {
|
|
69
|
-
return new Promise((resolveWatch, rejectWatch) => {
|
|
70
|
-
let bestAccuracy = Infinity;
|
|
71
|
-
let bestCoords = null;
|
|
72
|
-
let watchTimer;
|
|
73
|
-
|
|
74
|
-
locationWatchId.current = Geolocation.watchPosition(
|
|
75
|
-
(position) => {
|
|
76
|
-
const { coords } = position;
|
|
77
|
-
|
|
78
|
-
// Track best accuracy
|
|
79
|
-
if (coords.accuracy < bestAccuracy) {
|
|
80
|
-
bestAccuracy = coords.accuracy;
|
|
81
|
-
bestCoords = coords;
|
|
82
|
-
console.log(`[Location] Improved accuracy: ${coords.accuracy}m`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Stop if we reach desired accuracy
|
|
86
|
-
if (coords.accuracy <= 5) {
|
|
87
|
-
clearTimeout(watchTimer);
|
|
88
|
-
Geolocation.clearWatch(locationWatchId.current);
|
|
89
|
-
resolveWatch(coords);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
(error) => {
|
|
93
|
-
clearTimeout(watchTimer);
|
|
94
|
-
Geolocation.clearWatch(locationWatchId.current);
|
|
95
|
-
rejectWatch(error);
|
|
96
|
-
},
|
|
97
|
-
locationOptions
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Timeout after 15 seconds
|
|
101
|
-
watchTimer = setTimeout(() => {
|
|
102
|
-
Geolocation.clearWatch(locationWatchId.current);
|
|
103
|
-
resolveWatch(bestCoords);
|
|
104
|
-
}, 15000);
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
// First try: Single high-accuracy reading
|
|
110
|
-
const singleLocation = await new Promise((resolveSingle, rejectSingle) => {
|
|
111
|
-
Geolocation.getCurrentPosition(
|
|
112
|
-
resolveSingle,
|
|
113
|
-
rejectSingle,
|
|
114
|
-
locationOptions
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
bestLocation = singleLocation.coords;
|
|
119
|
-
console.log(`[Location] Initial accuracy: ${bestLocation.accuracy}m`);
|
|
120
|
-
|
|
121
|
-
// If accuracy > 20m, try watching for improvement
|
|
122
|
-
if (bestLocation.accuracy > 20) {
|
|
123
|
-
console.log('[Location] Accuracy insufficient, starting watch...');
|
|
124
|
-
const watchedCoords = await watchLocationForAccuracy();
|
|
125
|
-
if (watchedCoords && watchedCoords.accuracy < bestLocation.accuracy) {
|
|
126
|
-
bestLocation = watchedCoords;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
27
|
+
const result = await PermissionsAndroid.requestMultiple(permissions);
|
|
129
28
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
29
|
+
const granted = Object.values(result).every(
|
|
30
|
+
status => status === PermissionsAndroid.RESULTS.GRANTED
|
|
31
|
+
);
|
|
134
32
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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));
|
|
155
|
-
}
|
|
156
|
-
}),
|
|
157
|
-
[notifyMessage]
|
|
158
|
-
);
|
|
33
|
+
if (!granted) {
|
|
34
|
+
notifyMessage?.('Location permissions missing', 'warning');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return granted;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('[Geo Permissions]', err);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}, [notifyMessage]);
|
|
159
43
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
44
|
+
/* -------------------------------------------------------------------------- */
|
|
45
|
+
/* STOP LOCATION WATCH */
|
|
46
|
+
/* -------------------------------------------------------------------------- */
|
|
163
47
|
const stopLocationWatching = useCallback(() => {
|
|
164
48
|
if (locationWatchId.current !== null) {
|
|
165
|
-
console.log('[Location] Stopping location watch...');
|
|
166
49
|
Geolocation.clearWatch(locationWatchId.current);
|
|
167
50
|
locationWatchId.current = null;
|
|
168
51
|
}
|
|
169
52
|
}, []);
|
|
170
53
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
54
|
+
/* -------------------------------------------------------------------------- */
|
|
55
|
+
/* FAST + ACCURATE LOCATION FETCH */
|
|
56
|
+
/* -------------------------------------------------------------------------- */
|
|
57
|
+
const getCurrentLocation = useCallback((options = {}) => {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let samples = [];
|
|
60
|
+
let resolved = false;
|
|
61
|
+
|
|
62
|
+
const defaultOptions = {
|
|
63
|
+
enableHighAccuracy: true,
|
|
64
|
+
timeout: 8000, // ⏱ fast
|
|
65
|
+
maximumAge: 2000, // allow cached fix
|
|
66
|
+
forceRequestLocation: true,
|
|
67
|
+
showLocationDialog: true,
|
|
68
|
+
interval: 800,
|
|
69
|
+
fastestInterval: 500,
|
|
70
|
+
distanceFilter: 0,
|
|
71
|
+
...options,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
/* ---------------- FAST FIRST FIX ---------------- */
|
|
76
|
+
Geolocation.getCurrentPosition(
|
|
77
|
+
(pos) => {
|
|
78
|
+
if (!pos?.coords || resolved) return;
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
latitude,
|
|
82
|
+
longitude,
|
|
83
|
+
accuracy,
|
|
84
|
+
altitude = 0,
|
|
85
|
+
speed = 0,
|
|
86
|
+
heading = 0,
|
|
87
|
+
} = pos.coords;
|
|
88
|
+
|
|
89
|
+
// Accept immediately if good enough for verification
|
|
90
|
+
if (accuracy <= 15) {
|
|
91
|
+
resolved = true;
|
|
92
|
+
resolve({
|
|
93
|
+
latitude,
|
|
94
|
+
longitude,
|
|
95
|
+
altitude,
|
|
96
|
+
accuracy,
|
|
97
|
+
speed,
|
|
98
|
+
heading,
|
|
99
|
+
timestamp: pos.timestamp,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
samples.push({
|
|
104
|
+
latitude,
|
|
105
|
+
longitude,
|
|
106
|
+
altitude,
|
|
107
|
+
accuracy,
|
|
108
|
+
speed,
|
|
109
|
+
heading,
|
|
110
|
+
timestamp: pos.timestamp,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
(err) => {
|
|
114
|
+
console.warn('[Geo Fast Fix Failed]', err);
|
|
115
|
+
},
|
|
116
|
+
defaultOptions
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/* ---------------- BACKGROUND IMPROVEMENT ---------------- */
|
|
120
|
+
locationWatchId.current = Geolocation.watchPosition(
|
|
121
|
+
(p) => {
|
|
122
|
+
if (!p?.coords) return;
|
|
123
|
+
|
|
124
|
+
const {
|
|
125
|
+
latitude,
|
|
126
|
+
longitude,
|
|
127
|
+
accuracy,
|
|
128
|
+
altitude = 0,
|
|
129
|
+
speed = 0,
|
|
130
|
+
heading = 0,
|
|
131
|
+
} = p.coords;
|
|
132
|
+
|
|
133
|
+
/* -------- LIGHT NOISE FILTERS -------- */
|
|
134
|
+
if (accuracy > 40) return;
|
|
135
|
+
if (speed > 50) return;
|
|
136
|
+
|
|
137
|
+
samples.push({
|
|
138
|
+
latitude,
|
|
139
|
+
longitude,
|
|
140
|
+
altitude,
|
|
141
|
+
accuracy,
|
|
142
|
+
speed,
|
|
143
|
+
heading,
|
|
144
|
+
timestamp: p.timestamp,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (samples.length > 8) samples.shift();
|
|
148
|
+
|
|
149
|
+
samples.sort((a, b) => a.accuracy - b.accuracy);
|
|
150
|
+
const best = samples[0];
|
|
151
|
+
|
|
152
|
+
// Upgrade accuracy silently
|
|
153
|
+
if (best.accuracy <= 6 && !resolved) {
|
|
154
|
+
resolved = true;
|
|
155
|
+
stopLocationWatching();
|
|
156
|
+
resolve(best);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
(err) => {
|
|
160
|
+
console.error('[Geo Watch Error]', err);
|
|
161
|
+
stopLocationWatching();
|
|
162
|
+
},
|
|
163
|
+
defaultOptions
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
/* ---------------- HARD STOP ---------------- */
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (!resolved && samples.length) {
|
|
169
|
+
resolved = true;
|
|
170
|
+
stopLocationWatching();
|
|
171
|
+
samples.sort((a, b) => a.accuracy - b.accuracy);
|
|
172
|
+
resolve(samples[0]);
|
|
173
|
+
}
|
|
174
|
+
}, 5000);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
stopLocationWatching();
|
|
177
|
+
notifyMessage?.('Failed to get location', 'error');
|
|
178
|
+
reject(err);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}, [notifyMessage, stopLocationWatching]);
|
|
182
|
+
|
|
183
|
+
/* -------------------------------------------------------------------------- */
|
|
184
|
+
/* DISTANCE CALCULATION */
|
|
185
|
+
/* -------------------------------------------------------------------------- */
|
|
186
|
+
const calculateEnhancedDistance = useCallback(
|
|
187
|
+
(lat1, lon1, lat2, lon2, alt1 = 0, alt2 = 0) => {
|
|
188
|
+
try {
|
|
189
|
+
if (
|
|
190
|
+
typeof lat1 !== 'number' ||
|
|
191
|
+
typeof lon1 !== 'number' ||
|
|
192
|
+
typeof lat2 !== 'number' ||
|
|
193
|
+
typeof lon2 !== 'number'
|
|
194
|
+
) return Infinity;
|
|
195
|
+
|
|
196
|
+
const toRad = deg => deg * Math.PI / 180;
|
|
197
|
+
const R = 6371e3;
|
|
198
|
+
|
|
199
|
+
const φ1 = toRad(lat1);
|
|
200
|
+
const φ2 = toRad(lat2);
|
|
201
|
+
const Δφ = toRad(lat2 - lat1);
|
|
202
|
+
const Δλ = toRad(lon2 - lon1);
|
|
203
|
+
|
|
204
|
+
const a =
|
|
205
|
+
Math.sin(Δφ / 2) ** 2 +
|
|
206
|
+
Math.cos(φ1) * Math.cos(φ2) *
|
|
207
|
+
Math.sin(Δλ / 2) ** 2;
|
|
208
|
+
|
|
209
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
210
|
+
const surfaceDistance = R * c;
|
|
211
|
+
|
|
212
|
+
const heightDiff = Math.abs(alt1 - alt2);
|
|
213
|
+
return Math.sqrt(surfaceDistance ** 2 + heightDiff ** 2);
|
|
214
|
+
} catch {
|
|
215
|
+
return Infinity;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
[]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const calculateSafeDistance = useCallback(
|
|
222
|
+
(p1, p2) => {
|
|
223
|
+
try {
|
|
224
|
+
const lat1 = p1.latitude ?? p1.lat;
|
|
225
|
+
const lon1 = p1.longitude ?? p1.lng ?? p1.lon;
|
|
226
|
+
const alt1 = p1.altitude ?? p1.alt ?? 0;
|
|
227
|
+
|
|
228
|
+
const lat2 = p2.latitude ?? p2.lat;
|
|
229
|
+
const lon2 = p2.longitude ?? p2.lng ?? p2.lon;
|
|
230
|
+
const alt2 = p2.altitude ?? p2.alt ?? 0;
|
|
231
|
+
|
|
232
|
+
return calculateEnhancedDistance(
|
|
233
|
+
lat1, lon1, lat2, lon2, alt1, alt2
|
|
234
|
+
);
|
|
235
|
+
} catch {
|
|
236
|
+
return Infinity;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
[calculateEnhancedDistance]
|
|
240
|
+
);
|
|
195
241
|
|
|
242
|
+
/* -------------------------------------------------------------------------- */
|
|
243
|
+
/* API */
|
|
244
|
+
/* -------------------------------------------------------------------------- */
|
|
196
245
|
return {
|
|
197
|
-
requestLocationPermission
|
|
246
|
+
requestLocationPermission,
|
|
198
247
|
getCurrentLocation,
|
|
199
248
|
stopLocationWatching,
|
|
249
|
+
getCurrentWatchId: () => locationWatchId.current,
|
|
200
250
|
calculateEnhancedDistance,
|
|
251
|
+
calculateSafeDistance,
|
|
201
252
|
};
|
|
202
|
-
};
|
|
253
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Platform, PermissionsAndroid } from 'react-native';
|
|
3
|
+
import WifiManager from 'react-native-wifi-reborn';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WiFi Service Hook for WiFi scanning and fingerprinting
|
|
7
|
+
*/
|
|
8
|
+
export const useWifiService = (notifyMessage) => {
|
|
9
|
+
|
|
10
|
+
/* -------------------------------------------------------------------------- */
|
|
11
|
+
/* WIFI PERMISSIONS */
|
|
12
|
+
/* -------------------------------------------------------------------------- */
|
|
13
|
+
const requestWifiPermissions = useCallback(async () => {
|
|
14
|
+
try {
|
|
15
|
+
if (Platform.OS !== 'android') return false;
|
|
16
|
+
|
|
17
|
+
const permissions = [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
|
|
18
|
+
|
|
19
|
+
if (Platform.Version >= 31) {
|
|
20
|
+
permissions.push(PermissionsAndroid.PERMISSIONS.NEARBY_WIFI_DEVICES);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await PermissionsAndroid.requestMultiple(permissions);
|
|
24
|
+
|
|
25
|
+
const granted = Object.values(result).every(
|
|
26
|
+
status => status === PermissionsAndroid.RESULTS.GRANTED
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!granted) {
|
|
30
|
+
notifyMessage?.('WiFi scanning permissions missing', 'warning');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return granted;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[WiFi Permissions] Error:', err);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}, [notifyMessage]);
|
|
39
|
+
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
/* WIFI SCANNING */
|
|
42
|
+
/* -------------------------------------------------------------------------- */
|
|
43
|
+
const scanWifiFingerprint = useCallback(async () => {
|
|
44
|
+
if (Platform.OS !== 'android') return [];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const hasPermission = await requestWifiPermissions();
|
|
48
|
+
if (!hasPermission) return [];
|
|
49
|
+
|
|
50
|
+
const list = await WifiManager.loadWifiList();
|
|
51
|
+
|
|
52
|
+
const fingerprint = list.map(ap => ({
|
|
53
|
+
bssid: ap.BSSID,
|
|
54
|
+
ssid: ap.SSID || 'Unknown',
|
|
55
|
+
rssi: ap.level,
|
|
56
|
+
frequency: ap.frequency || 0,
|
|
57
|
+
capabilities: ap.capabilities || '',
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
return fingerprint;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[WiFi] Scan failed:', err);
|
|
64
|
+
notifyMessage?.('WiFi scan failed', 'error');
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}, [requestWifiPermissions, notifyMessage]);
|
|
68
|
+
|
|
69
|
+
/* -------------------------------------------------------------------------- */
|
|
70
|
+
/* WIFI FINGERPRINT MATCHING */
|
|
71
|
+
/* -------------------------------------------------------------------------- */
|
|
72
|
+
const matchWifiFingerprint = useCallback((scan, reference, options = {}) => {
|
|
73
|
+
const { rssiThreshold = 10, matchWeight = 1, partialMatchWeight = 0.5 } = options;
|
|
74
|
+
|
|
75
|
+
if (!scan || !reference || !Array.isArray(scan) || !Array.isArray(reference)) return 0;
|
|
76
|
+
|
|
77
|
+
let score = 0;
|
|
78
|
+
let matches = 0;
|
|
79
|
+
const totalPossibleMatches = Math.min(scan.length, reference.length);
|
|
80
|
+
|
|
81
|
+
scan.forEach(ap => {
|
|
82
|
+
const match = reference.find(r => r.bssid === ap.bssid);
|
|
83
|
+
if (match) {
|
|
84
|
+
const diff = Math.abs((match.rssi || 0) - ap.rssi);
|
|
85
|
+
score += diff <= rssiThreshold ? matchWeight : partialMatchWeight;
|
|
86
|
+
matches++;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const matchPercentage = totalPossibleMatches > 0 ? (matches / totalPossibleMatches) * 100 : 0;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
score,
|
|
94
|
+
matches,
|
|
95
|
+
totalPossibleMatches,
|
|
96
|
+
matchPercentage,
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
/* -------------------------------------------------------------------------- */
|
|
101
|
+
/* WIFI UTILITIES */
|
|
102
|
+
/* -------------------------------------------------------------------------- */
|
|
103
|
+
const getWifiNetworksByStrength = useCallback((networks, limit = 5) => {
|
|
104
|
+
if (!Array.isArray(networks)) return [];
|
|
105
|
+
return networks
|
|
106
|
+
.filter(network => network && typeof network.rssi === 'number')
|
|
107
|
+
.sort((a, b) => b.rssi - a.rssi)
|
|
108
|
+
.slice(0, limit);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const filterWifiNetworks = useCallback((networks, options = {}) => {
|
|
112
|
+
const { minRssi = -90, maxRssi = -30, excludeHidden = true } = options;
|
|
113
|
+
if (!Array.isArray(networks)) return [];
|
|
114
|
+
|
|
115
|
+
return networks.filter(network => {
|
|
116
|
+
if (!network || !network.bssid) return false;
|
|
117
|
+
if (network.rssi < minRssi || network.rssi > maxRssi) return false;
|
|
118
|
+
if (excludeHidden && (!network.ssid || network.ssid === '' || network.ssid === '<unknown ssid>')) return false;
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const getCurrentWifiInfo = useCallback(async () => {
|
|
124
|
+
if (Platform.OS !== 'android') return null;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const hasPermission = await requestWifiPermissions();
|
|
128
|
+
if (!hasPermission) return null;
|
|
129
|
+
|
|
130
|
+
const currentWifi = await WifiManager.getCurrentWifiSSID();
|
|
131
|
+
return { ssid: currentWifi, timestamp: Date.now() };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('[WiFi] Failed to get current WiFi:', err);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}, [requestWifiPermissions]);
|
|
137
|
+
|
|
138
|
+
/* -------------------------------------------------------------------------- */
|
|
139
|
+
/* COMBINED LOCATION SCAN */
|
|
140
|
+
/* -------------------------------------------------------------------------- */
|
|
141
|
+
const getLocationWithWifi = useCallback(async (geolocationHook) => {
|
|
142
|
+
try {
|
|
143
|
+
const wifiPromise = scanWifiFingerprint();
|
|
144
|
+
|
|
145
|
+
let locationResult;
|
|
146
|
+
try {
|
|
147
|
+
locationResult = await geolocationHook.getCurrentLocation();
|
|
148
|
+
} catch {
|
|
149
|
+
locationResult = null; // continue with WiFi only
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const wifiFingerprint = await wifiPromise;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...(locationResult || {}),
|
|
156
|
+
wifi: wifiFingerprint,
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
source: locationResult ? 'gps+wifi' : 'wifi-only',
|
|
159
|
+
};
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('[WiFi+Location] Combined scan failed:', err);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}, [scanWifiFingerprint]);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
requestWifiPermissions,
|
|
168
|
+
scanWifiFingerprint,
|
|
169
|
+
getCurrentWifiInfo,
|
|
170
|
+
matchWifiFingerprint,
|
|
171
|
+
getWifiNetworksByStrength,
|
|
172
|
+
filterWifiNetworks,
|
|
173
|
+
getLocationWithWifi,
|
|
174
|
+
};
|
|
175
|
+
};
|
package/src/index.js
CHANGED
|
@@ -18,10 +18,9 @@ import {
|
|
|
18
18
|
} from "react-native";
|
|
19
19
|
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
20
20
|
|
|
21
|
-
// Custom hooks
|
|
21
|
+
// Custom hooks - Removed unnecessary ones
|
|
22
22
|
import { useCountdown } from "./hooks/useCountdown";
|
|
23
23
|
import { useGeolocation } from "./hooks/useGeolocation";
|
|
24
|
-
import { useBluetoothService } from "./hooks/useBluetoothService";
|
|
25
24
|
import { useImageProcessing } from "./hooks/useImageProcessing";
|
|
26
25
|
import { useNotifyMessage } from "./hooks/useNotifyMessage";
|
|
27
26
|
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
@@ -51,42 +50,35 @@ const BiometricModal = forwardRef(({
|
|
|
51
50
|
fileurl,
|
|
52
51
|
imageurl,
|
|
53
52
|
navigation,
|
|
54
|
-
MaxDistanceMeters =
|
|
55
|
-
duration = 100
|
|
53
|
+
MaxDistanceMeters = 30,
|
|
54
|
+
duration = 100,
|
|
56
55
|
}, ref) => {
|
|
57
|
-
// Custom hooks - Initialize notification hook first
|
|
56
|
+
// Custom hooks - Initialize notification hook first
|
|
58
57
|
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
59
58
|
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
60
|
-
|
|
61
|
-
//
|
|
59
|
+
|
|
60
|
+
// Only keep geolocation hook
|
|
62
61
|
const {
|
|
63
62
|
requestLocationPermission,
|
|
64
63
|
getCurrentLocation,
|
|
65
64
|
stopLocationWatching,
|
|
66
|
-
|
|
65
|
+
calculateSafeDistance,
|
|
67
66
|
} = useGeolocation(notifyMessage);
|
|
68
|
-
|
|
69
|
-
const {
|
|
70
|
-
requestBluetoothPermission,
|
|
71
|
-
startBluetoothScan,
|
|
72
|
-
stopBluetoothScan,
|
|
73
|
-
nearbyDevices,
|
|
74
|
-
clearDevices,
|
|
75
|
-
} = useBluetoothService(notifyMessage);
|
|
76
|
-
|
|
67
|
+
|
|
77
68
|
const { convertImageToBase64 } = useImageProcessing();
|
|
78
69
|
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
79
70
|
|
|
80
|
-
// State
|
|
71
|
+
// State - Simplified
|
|
81
72
|
const [modalVisible, setModalVisible] = useState(false);
|
|
82
|
-
const [cameraType, setCameraType] = useState("back");
|
|
73
|
+
const [cameraType, setCameraType] = useState("back");
|
|
83
74
|
const [state, setState] = useState({
|
|
84
75
|
isLoading: false,
|
|
85
76
|
loadingType: Global.LoadingTypes.none,
|
|
86
77
|
currentStep: "Start",
|
|
87
78
|
employeeData: null,
|
|
88
|
-
animationState: Global.AnimationStates.qrScan,
|
|
89
|
-
qrData: null,
|
|
79
|
+
animationState: Global.AnimationStates.qrScan,
|
|
80
|
+
qrData: null,
|
|
81
|
+
// Removed: wifiReferenceScan
|
|
90
82
|
});
|
|
91
83
|
|
|
92
84
|
// Refs
|
|
@@ -95,8 +87,8 @@ const BiometricModal = forwardRef(({
|
|
|
95
87
|
const responseRef = useRef(null);
|
|
96
88
|
const processedRef = useRef(false);
|
|
97
89
|
const resetTimeoutRef = useRef(null);
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
// Removed: bleScanTimeoutRef, wifiScanTimeoutRef
|
|
91
|
+
|
|
100
92
|
// Animation values
|
|
101
93
|
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
102
94
|
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
@@ -109,7 +101,7 @@ const BiometricModal = forwardRef(({
|
|
|
109
101
|
getStatus: () => state,
|
|
110
102
|
}));
|
|
111
103
|
|
|
112
|
-
// Cleanup on unmount
|
|
104
|
+
// Cleanup on unmount - Simplified
|
|
113
105
|
useEffect(() => {
|
|
114
106
|
return () => {
|
|
115
107
|
mountedRef.current = false;
|
|
@@ -117,17 +109,11 @@ const BiometricModal = forwardRef(({
|
|
|
117
109
|
if (resetTimeoutRef.current) {
|
|
118
110
|
clearTimeout(resetTimeoutRef.current);
|
|
119
111
|
}
|
|
120
|
-
|
|
121
|
-
if (bleScanTimeoutRef.current) {
|
|
122
|
-
clearTimeout(bleScanTimeoutRef.current);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
stopBluetoothScan();
|
|
112
|
+
|
|
126
113
|
stopLocationWatching();
|
|
127
|
-
clearDevices();
|
|
128
114
|
clearNotification();
|
|
129
115
|
};
|
|
130
|
-
}, [
|
|
116
|
+
}, [stopLocationWatching, clearNotification]);
|
|
131
117
|
|
|
132
118
|
// Update dataRef when data changes
|
|
133
119
|
useEffect(() => {
|
|
@@ -136,11 +122,9 @@ const BiometricModal = forwardRef(({
|
|
|
136
122
|
|
|
137
123
|
// Animation helper
|
|
138
124
|
const animateIcon = useCallback(() => {
|
|
139
|
-
// Reset animation
|
|
140
125
|
iconScaleAnim.setValue(1);
|
|
141
126
|
iconOpacityAnim.setValue(0);
|
|
142
127
|
|
|
143
|
-
// Start animation sequence
|
|
144
128
|
Animated.sequence([
|
|
145
129
|
Animated.parallel([
|
|
146
130
|
Animated.timing(iconOpacityAnim, {
|
|
@@ -169,7 +153,6 @@ const BiometricModal = forwardRef(({
|
|
|
169
153
|
const merged = { ...prev, ...newState };
|
|
170
154
|
|
|
171
155
|
if (JSON.stringify(prev) !== JSON.stringify(merged)) {
|
|
172
|
-
// Pause/resume countdown based on loading state
|
|
173
156
|
if (newState.isLoading !== undefined) {
|
|
174
157
|
if (newState.isLoading) {
|
|
175
158
|
pauseCountdown();
|
|
@@ -178,7 +161,6 @@ const BiometricModal = forwardRef(({
|
|
|
178
161
|
}
|
|
179
162
|
}
|
|
180
163
|
|
|
181
|
-
// Animate icon when step changes
|
|
182
164
|
if (newState.currentStep && newState.currentStep !== prev.currentStep) {
|
|
183
165
|
animateIcon();
|
|
184
166
|
}
|
|
@@ -191,7 +173,7 @@ const BiometricModal = forwardRef(({
|
|
|
191
173
|
}
|
|
192
174
|
}, [animateIcon, pauseCountdown, resumeCountdown]);
|
|
193
175
|
|
|
194
|
-
// Reset state helper
|
|
176
|
+
// Reset state helper - Simplified
|
|
195
177
|
const resetState = useCallback(() => {
|
|
196
178
|
if (onclose) {
|
|
197
179
|
onclose(false);
|
|
@@ -209,16 +191,14 @@ const BiometricModal = forwardRef(({
|
|
|
209
191
|
setModalVisible(false);
|
|
210
192
|
processedRef.current = false;
|
|
211
193
|
resetCountdown();
|
|
212
|
-
stopBluetoothScan();
|
|
213
194
|
stopLocationWatching();
|
|
214
|
-
clearDevices();
|
|
215
195
|
clearNotification();
|
|
216
196
|
|
|
217
197
|
if (resetTimeoutRef.current) {
|
|
218
198
|
clearTimeout(resetTimeoutRef.current);
|
|
219
199
|
resetTimeoutRef.current = null;
|
|
220
200
|
}
|
|
221
|
-
}, [resetCountdown,
|
|
201
|
+
}, [resetCountdown, stopLocationWatching, clearNotification, onclose]);
|
|
222
202
|
|
|
223
203
|
// Error handler
|
|
224
204
|
const handleProcessError = useCallback(
|
|
@@ -264,49 +244,7 @@ const BiometricModal = forwardRef(({
|
|
|
264
244
|
return true;
|
|
265
245
|
}, [apiurl, handleProcessError]);
|
|
266
246
|
|
|
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
|
-
|
|
308
|
-
// QR code processing - FIRST STEP
|
|
309
|
-
// Enhanced QR processing
|
|
247
|
+
// Simplified QR scanning handler - GPS + Key verification only
|
|
310
248
|
const handleQRScanned = useCallback(
|
|
311
249
|
async (qrCodeData) => {
|
|
312
250
|
if (!validateApiUrl()) return;
|
|
@@ -318,7 +256,7 @@ const BiometricModal = forwardRef(({
|
|
|
318
256
|
});
|
|
319
257
|
|
|
320
258
|
try {
|
|
321
|
-
// 1. Request
|
|
259
|
+
// 1. Request location permission
|
|
322
260
|
updateState({ loadingType: Global.LoadingTypes.locationPermission });
|
|
323
261
|
const hasLocationPermission = await requestLocationPermission();
|
|
324
262
|
if (!hasLocationPermission) {
|
|
@@ -326,12 +264,6 @@ const BiometricModal = forwardRef(({
|
|
|
326
264
|
return;
|
|
327
265
|
}
|
|
328
266
|
|
|
329
|
-
const hasBluetoothPermission = await requestBluetoothPermission();
|
|
330
|
-
if (!hasBluetoothPermission) {
|
|
331
|
-
notifyMessage("Bluetooth scanning may not work", "warning");
|
|
332
|
-
// Continue anyway, Bluetooth is optional
|
|
333
|
-
}
|
|
334
|
-
|
|
335
267
|
// 2. Parse QR data with validation
|
|
336
268
|
const qrString = typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
|
|
337
269
|
if (!qrString || typeof qrString !== "string") {
|
|
@@ -339,7 +271,7 @@ const BiometricModal = forwardRef(({
|
|
|
339
271
|
return;
|
|
340
272
|
}
|
|
341
273
|
|
|
342
|
-
// 3. Parse and validate QR coordinates
|
|
274
|
+
// 3. Parse and validate QR coordinates
|
|
343
275
|
const parts = qrString.split(",");
|
|
344
276
|
|
|
345
277
|
if (parts.length < 3) {
|
|
@@ -349,10 +281,8 @@ const BiometricModal = forwardRef(({
|
|
|
349
281
|
|
|
350
282
|
const latStr = parts[0];
|
|
351
283
|
const lngStr = parts[1];
|
|
352
|
-
const qrDepKey = parts[2];
|
|
353
|
-
|
|
354
|
-
// Optional: QR code accuracy if provided as fourth parameter
|
|
355
|
-
const qrAccuracy = parts.length > 3 ? parseFloat(parts[3]) : 5; // Default 5m accuracy
|
|
284
|
+
const qrDepKey = parts[2];
|
|
285
|
+
const qrAccuracy = parts.length > 3 ? parseFloat(parts[3]) : 5;
|
|
356
286
|
|
|
357
287
|
const qrLat = parseFloat(latStr);
|
|
358
288
|
const qrLng = parseFloat(lngStr);
|
|
@@ -362,55 +292,56 @@ const BiometricModal = forwardRef(({
|
|
|
362
292
|
return;
|
|
363
293
|
}
|
|
364
294
|
|
|
365
|
-
//
|
|
295
|
+
// 4. Get high-accuracy location
|
|
366
296
|
updateState({ loadingType: Global.LoadingTypes.gettingLocation });
|
|
367
|
-
|
|
297
|
+
|
|
298
|
+
let location;
|
|
299
|
+
try {
|
|
300
|
+
location = await getCurrentLocation();
|
|
301
|
+
} catch (locationError) {
|
|
302
|
+
console.error('Location error:', locationError);
|
|
303
|
+
handleProcessError("Failed to get location. Please try again.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Validate location object
|
|
308
|
+
if (!location || typeof location !== 'object') {
|
|
309
|
+
handleProcessError("Invalid location data received.");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validate location coordinates
|
|
314
|
+
if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number' ||
|
|
315
|
+
isNaN(location.latitude) || isNaN(location.longitude)) {
|
|
316
|
+
handleProcessError("Invalid GPS coordinates received.");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
368
319
|
|
|
369
320
|
// Validate location accuracy
|
|
370
321
|
if (location.accuracy > 50) {
|
|
371
322
|
notifyMessage(`Location accuracy is ${location.accuracy.toFixed(1)}m. For best results, move to an open area.`, 'warning');
|
|
372
323
|
}
|
|
373
324
|
|
|
374
|
-
//
|
|
375
|
-
updateState({ loadingType: Global.LoadingTypes.bleScan });
|
|
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();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 5. Calculate distance (no altitude in your QR code)
|
|
325
|
+
// 5. Calculate distance using safe calculation
|
|
401
326
|
updateState({ loadingType: Global.LoadingTypes.calculateDistance });
|
|
402
327
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
location.latitude, location.longitude
|
|
328
|
+
const distance = calculateSafeDistance(
|
|
329
|
+
{ latitude: qrLat, longitude: qrLng },
|
|
330
|
+
{ latitude: location.latitude, longitude: location.longitude }
|
|
407
331
|
);
|
|
408
332
|
|
|
333
|
+
// Validate distance calculation
|
|
334
|
+
if (distance === Infinity || isNaN(distance)) {
|
|
335
|
+
handleProcessError("Failed to calculate distance. Please try again.");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
409
339
|
const distanceWithinThreshold = distance <= MaxDistanceMeters;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
340
|
+
|
|
341
|
+
// Simple verification criteria - GPS + Key only
|
|
342
|
+
const verificationPassed = distanceWithinThreshold && qrDepKey === depkey;
|
|
343
|
+
|
|
344
|
+
if (verificationPassed) {
|
|
414
345
|
const locationDetails = {
|
|
415
346
|
qrLocation: {
|
|
416
347
|
latitude: qrLat,
|
|
@@ -422,15 +353,15 @@ const BiometricModal = forwardRef(({
|
|
|
422
353
|
longitude: location.longitude,
|
|
423
354
|
altitude: location.altitude || 0,
|
|
424
355
|
accuracy: location.accuracy,
|
|
425
|
-
speed: location.speed,
|
|
426
|
-
heading: location.heading,
|
|
427
|
-
timestamp: location.timestamp
|
|
356
|
+
speed: location.speed || 0,
|
|
357
|
+
heading: location.heading || 0,
|
|
358
|
+
timestamp: location.timestamp || Date.now()
|
|
428
359
|
},
|
|
429
360
|
distanceMeters: distance,
|
|
430
361
|
accuracyBuffer: MaxDistanceMeters,
|
|
431
362
|
verified: true,
|
|
363
|
+
verificationMethod: "GPS+Key",
|
|
432
364
|
verifiedAt: new Date().toISOString(),
|
|
433
|
-
bleDevicesDetected: consistentDevices,
|
|
434
365
|
locationMethod: location.provider || 'unknown',
|
|
435
366
|
qrData: qrString,
|
|
436
367
|
qrKey: qrDepKey
|
|
@@ -445,38 +376,34 @@ const BiometricModal = forwardRef(({
|
|
|
445
376
|
loadingType: Global.LoadingTypes.none,
|
|
446
377
|
});
|
|
447
378
|
|
|
448
|
-
|
|
379
|
+
const successMessage = `Location verified! Distance: ${distance.toFixed(1)}m (±${Math.max(location.accuracy || 0, qrAccuracy).toFixed(1)}m)`;
|
|
380
|
+
notifyMessage(successMessage, "success");
|
|
449
381
|
setTimeout(() => startFaceRecognition(), 1200);
|
|
450
382
|
} else {
|
|
451
|
-
let errorMsg = `
|
|
383
|
+
let errorMsg = `Verification failed: ${distance.toFixed(1)}m away`;
|
|
452
384
|
if (qrDepKey !== depkey) errorMsg += " (Key mismatch)";
|
|
453
385
|
handleProcessError(errorMsg);
|
|
454
386
|
}
|
|
455
387
|
} catch (error) {
|
|
456
|
-
console.error("
|
|
457
|
-
handleProcessError("Unable to verify location
|
|
388
|
+
console.error("Location verification failed:", error);
|
|
389
|
+
handleProcessError("Unable to verify location. Please try again.", error);
|
|
458
390
|
}
|
|
459
391
|
},
|
|
460
392
|
[
|
|
461
393
|
validateApiUrl,
|
|
462
394
|
updateState,
|
|
463
395
|
requestLocationPermission,
|
|
464
|
-
requestBluetoothPermission,
|
|
465
396
|
getCurrentLocation,
|
|
466
|
-
startBluetoothScan,
|
|
467
|
-
stopBluetoothScan,
|
|
468
|
-
nearbyDevices,
|
|
469
|
-
findConsistentBLEDevices,
|
|
470
397
|
notifyMessage,
|
|
471
398
|
handleProcessError,
|
|
472
399
|
startFaceRecognition,
|
|
473
400
|
depkey,
|
|
474
401
|
MaxDistanceMeters,
|
|
475
|
-
|
|
402
|
+
calculateSafeDistance,
|
|
476
403
|
]
|
|
477
404
|
);
|
|
478
405
|
|
|
479
|
-
// Face scan upload - SECOND STEP
|
|
406
|
+
// Face scan upload - SECOND STEP (simplified, no WiFi revalidation)
|
|
480
407
|
const uploadFaceScan = useCallback(
|
|
481
408
|
async (selfie) => {
|
|
482
409
|
if (!validateApiUrl()) return;
|
|
@@ -539,7 +466,7 @@ const BiometricModal = forwardRef(({
|
|
|
539
466
|
if (response?.httpstatus === 200) {
|
|
540
467
|
// Combine face recognition response with QR location data
|
|
541
468
|
responseRef.current = {
|
|
542
|
-
...responseRef.current,
|
|
469
|
+
...responseRef.current,
|
|
543
470
|
...response.data?.data || {},
|
|
544
471
|
faceRecognition: response.data?.data || null,
|
|
545
472
|
};
|
|
@@ -587,7 +514,7 @@ const BiometricModal = forwardRef(({
|
|
|
587
514
|
safeCallback,
|
|
588
515
|
handleProcessError,
|
|
589
516
|
state.qrData,
|
|
590
|
-
apiurl
|
|
517
|
+
apiurl,
|
|
591
518
|
]
|
|
592
519
|
);
|
|
593
520
|
|
|
@@ -758,6 +685,7 @@ const styles = StyleSheet.create({
|
|
|
758
685
|
position: 'absolute',
|
|
759
686
|
bottom: Platform.OS === 'ios' ? 50 : 30,
|
|
760
687
|
left: 0,
|
|
688
|
+
right: 0,
|
|
761
689
|
zIndex: 10,
|
|
762
690
|
},
|
|
763
691
|
headerContainer: {
|
|
@@ -793,6 +721,16 @@ const styles = StyleSheet.create({
|
|
|
793
721
|
textShadowOffset: { width: 1, height: 1 },
|
|
794
722
|
textShadowRadius: 2,
|
|
795
723
|
},
|
|
724
|
+
wifiIndicator: {
|
|
725
|
+
fontSize: 12,
|
|
726
|
+
color: Global.AppTheme.primary || '#4CAF50',
|
|
727
|
+
marginTop: 4,
|
|
728
|
+
fontWeight: '500',
|
|
729
|
+
fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
|
|
730
|
+
textShadowColor: 'rgba(0, 0, 0, 0.2)',
|
|
731
|
+
textShadowOffset: { width: 1, height: 1 },
|
|
732
|
+
textShadowRadius: 1,
|
|
733
|
+
},
|
|
796
734
|
closeButton: {
|
|
797
735
|
position: 'absolute',
|
|
798
736
|
top: Platform.OS === 'ios' ? 40 : 20,
|