react-native-biometric-verifier 0.0.43 → 0.0.45

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.43",
3
+ "version": "0.0.45",
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,
@@ -1,32 +1,23 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
  import { Animated, Text, Platform, StyleSheet } from 'react-native';
3
3
  import Icon from 'react-native-vector-icons/MaterialIcons';
4
- import PropTypes from 'prop-types';
5
4
  import { Global } from '../utils/Global';
6
5
 
7
- export const Notification = ({ notification, fadeAnim, slideAnim }) => {
8
- if (!notification || typeof notification !== 'object') {
9
- console.warn('Notification: Invalid or missing notification object');
10
- return null;
11
- }
6
+ export const Notification = ({ notification = {}, fadeAnim, slideAnim }) => {
7
+ const { visible = false, type = 'info', message = '' } = notification;
12
8
 
13
- const { visible, type = 'info', message = '' } = notification;
14
- if (!visible) return null;
9
+ // Animations (must ALWAYS exist)
10
+ const scaleAnim = useRef(new Animated.Value(1)).current;
11
+ const shakeAnim = useRef(new Animated.Value(0)).current;
15
12
 
16
- // Icon and color mapping
17
13
  const iconMap = {
18
14
  success: { name: 'check-circle', color: Global.AppTheme.success },
19
15
  error: { name: 'error', color: Global.AppTheme.error },
20
16
  info: { name: 'info', color: Global.AppTheme.info },
21
17
  };
22
18
 
23
- const { name: iconName, color: iconColor } = iconMap[type] || iconMap.info;
24
-
25
- // Heartbeat animation (scale in/out)
26
- const scaleAnim = useRef(new Animated.Value(1)).current;
27
-
28
- // Shake animation (rotation wiggle)
29
- const shakeAnim = useRef(new Animated.Value(0)).current;
19
+ const { name: iconName, color: iconColor } =
20
+ iconMap[type] || iconMap.info;
30
21
 
31
22
  useEffect(() => {
32
23
  const heartbeat = Animated.loop(
@@ -78,20 +69,20 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
78
69
  heartbeat.stop();
79
70
  shake.stop();
80
71
  };
81
- }, [visible, type, scaleAnim, shakeAnim]);
72
+ }, [visible, type]);
82
73
 
83
- // Shake rotation mapping (small wiggle)
84
- const shakeInterpolate = shakeAnim.interpolate({
74
+ const shakeRotate = shakeAnim.interpolate({
85
75
  inputRange: [-1, 1],
86
76
  outputRange: ['-10deg', '10deg'],
87
77
  });
88
78
 
89
79
  return (
90
80
  <Animated.View
81
+ pointerEvents={visible ? 'auto' : 'none'}
91
82
  style={[
92
83
  styles.container,
93
84
  {
94
- opacity: fadeAnim instanceof Animated.Value ? fadeAnim : 1,
85
+ opacity: visible ? 1 : 0,
95
86
  transform: [
96
87
  { translateY: slideAnim instanceof Animated.Value ? slideAnim : 0 },
97
88
  ],
@@ -102,12 +93,13 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
102
93
  style={{
103
94
  transform: [
104
95
  { scale: scaleAnim },
105
- ...(type === 'error' ? [{ rotate: shakeInterpolate }] : []),
96
+ ...(type === 'error' ? [{ rotate: shakeRotate }] : []),
106
97
  ],
107
98
  }}
108
99
  >
109
- <Icon name={iconName} size={50} color={iconColor} style={styles.icon} />
100
+ <Icon name={iconName} size={50} color={iconColor} />
110
101
  </Animated.View>
102
+
111
103
  <Text style={styles.message}>
112
104
  {message || 'No message provided'}
113
105
  </Text>
@@ -142,20 +134,4 @@ const styles = StyleSheet.create({
142
134
  },
143
135
  });
144
136
 
145
- Notification.propTypes = {
146
- notification: PropTypes.shape({
147
- visible: PropTypes.bool.isRequired,
148
- type: PropTypes.oneOf(['success', 'error', 'info']),
149
- message: PropTypes.string,
150
- }),
151
- fadeAnim: PropTypes.instanceOf(Animated.Value),
152
- slideAnim: PropTypes.instanceOf(Animated.Value),
153
- };
154
-
155
- Notification.defaultProps = {
156
- notification: { visible: false, type: 'info', message: '' },
157
- fadeAnim: new Animated.Value(1),
158
- slideAnim: new Animated.Value(0),
159
- };
160
-
161
137
  export default Notification;
@@ -1,71 +1,299 @@
1
- import { useCallback } from 'react';
1
+ // Enhanced location service
2
+ import { useCallback, useState, useRef } from 'react';
2
3
  import { Platform, PermissionsAndroid } from 'react-native';
3
4
  import Geolocation from 'react-native-geolocation-service';
5
+ import { BleManager } from 'react-native-ble-plx';
6
+
7
+ const manager = new BleManager();
4
8
 
5
9
  /**
6
- * Custom hook for requesting location permission and fetching current location.
7
- *
8
- * @param {Function} notifyMessage - Callback to show notifications (message, type).
9
- * @returns {Object} { requestLocationPermission, getCurrentLocation }
10
+ * Enhanced Geolocation Hook with better accuracy
10
11
  */
11
12
  export const useGeolocation = (notifyMessage) => {
13
+ const [nearbyDevices, setNearbyDevices] = useState([]);
14
+ const scanTimeoutRef = useRef(null);
15
+ const locationWatchId = useRef(null);
16
+
12
17
  /**
13
- * Requests location permission (Android only).
14
- * @returns {Promise<boolean>} - True if permission granted, else false.
18
+ * Request high-accuracy permissions
15
19
  */
16
- const requestLocationPermission = useCallback(async () => {
17
- if (Platform.OS !== 'android') return true;
18
-
19
- try {
20
- const granted = await PermissionsAndroid.request(
21
- PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
22
- {
23
- title: 'Location Permission Required',
24
- message: 'This app needs your location to verify QR code scans.',
25
- buttonPositive: 'OK',
26
- buttonNegative: 'Cancel',
27
- }
28
- );
20
+ const requestPermissions = useCallback(async () => {
21
+ console.log('[Permissions] Requesting enhanced permissions...');
22
+
23
+ if (Platform.OS === 'android') {
24
+ try {
25
+ const granted = await PermissionsAndroid.requestMultiple([
26
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
27
+ PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
28
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
29
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
30
+ ]);
29
31
 
30
- const hasPermission = granted === PermissionsAndroid.RESULTS.GRANTED;
32
+ // Check Android version for background location
33
+ if (Platform.Version >= 29) {
34
+ await PermissionsAndroid.request(
35
+ PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION
36
+ );
37
+ }
31
38
 
32
- if (!hasPermission) {
33
- notifyMessage?.('Cannot get location without permission.', 'error');
39
+ return Object.values(granted).every(
40
+ status => status === PermissionsAndroid.RESULTS.GRANTED
41
+ );
42
+ } catch (error) {
43
+ console.error('[Permissions] Error:', error);
44
+ return false;
34
45
  }
35
-
36
- return hasPermission;
37
- } catch (error) {
38
- console.error('Location permission error:', error);
39
- notifyMessage?.('Location permission error.', 'error');
40
- return false;
41
46
  }
42
- }, [notifyMessage]);
47
+ return true; // iOS handles permissions differently
48
+ }, []);
43
49
 
44
50
  /**
45
- * Fetches the current location of the device.
46
- * @returns {Promise<GeolocationCoordinates>}
51
+ * Get high-accuracy location with multiple attempts
47
52
  */
48
53
  const getCurrentLocation = useCallback(
49
54
  () =>
50
- new Promise((resolve, reject) => {
51
- Geolocation.getCurrentPosition(
52
- (position) => {
53
- resolve(position.coords);
54
- },
55
- (error) => {
56
- notifyMessage?.('Unable to fetch current location.', 'error');
57
- reject(error);
58
- },
59
- {
60
- enableHighAccuracy: true,
61
- timeout: 15000,
62
- maximumAge: 10000,
63
- distanceFilter: 1,
55
+ new Promise(async (resolve, reject) => {
56
+ console.log('[Location] Fetching enhanced location...');
57
+
58
+ let bestLocation = null;
59
+ let attempts = 0;
60
+ const maxAttempts = 3;
61
+
62
+ const locationOptions = {
63
+ enableHighAccuracy: true,
64
+ timeout: 20000,
65
+ maximumAge: 0,
66
+ distanceFilter: 0,
67
+ forceRequestLocation: true,
68
+ showLocationDialog: true,
69
+ };
70
+
71
+ const watchLocationForAccuracy = () => {
72
+ return new Promise((resolveWatch, rejectWatch) => {
73
+ let bestAccuracy = Infinity;
74
+ let bestCoords = null;
75
+ let watchTimer;
76
+
77
+ locationWatchId.current = Geolocation.watchPosition(
78
+ (position) => {
79
+ const { coords } = position;
80
+
81
+ // Track best accuracy
82
+ if (coords.accuracy < bestAccuracy) {
83
+ bestAccuracy = coords.accuracy;
84
+ bestCoords = coords;
85
+ console.log(`[Location] Improved accuracy: ${coords.accuracy}m`);
86
+ }
87
+
88
+ // Stop if we reach desired accuracy
89
+ if (coords.accuracy <= 5) {
90
+ clearTimeout(watchTimer);
91
+ Geolocation.clearWatch(locationWatchId.current);
92
+ resolveWatch(coords);
93
+ }
94
+ },
95
+ (error) => {
96
+ clearTimeout(watchTimer);
97
+ Geolocation.clearWatch(locationWatchId.current);
98
+ rejectWatch(error);
99
+ },
100
+ locationOptions
101
+ );
102
+
103
+ // Timeout after 15 seconds
104
+ watchTimer = setTimeout(() => {
105
+ Geolocation.clearWatch(locationWatchId.current);
106
+ resolveWatch(bestCoords);
107
+ }, 15000);
108
+ });
109
+ };
110
+
111
+ try {
112
+ // First try: Single high-accuracy reading
113
+ const singleLocation = await new Promise((resolveSingle, rejectSingle) => {
114
+ Geolocation.getCurrentPosition(
115
+ resolveSingle,
116
+ rejectSingle,
117
+ locationOptions
118
+ );
119
+ });
120
+
121
+ bestLocation = singleLocation.coords;
122
+ console.log(`[Location] Initial accuracy: ${bestLocation.accuracy}m`);
123
+
124
+ // If accuracy > 20m, try watching for improvement
125
+ if (bestLocation.accuracy > 20) {
126
+ console.log('[Location] Accuracy insufficient, starting watch...');
127
+ const watchedCoords = await watchLocationForAccuracy();
128
+ if (watchedCoords && watchedCoords.accuracy < bestLocation.accuracy) {
129
+ bestLocation = watchedCoords;
130
+ }
64
131
  }
65
- );
132
+
133
+ // Validate location quality
134
+ if (!bestLocation || bestLocation.accuracy > 50) {
135
+ notifyMessage?.('Location accuracy is low. Please move to an open area.', 'warning');
136
+ }
137
+
138
+ resolve(bestLocation);
139
+ } catch (error) {
140
+ console.error('[Location] Error:', error);
141
+ reject(error);
142
+ }
66
143
  }),
67
144
  [notifyMessage]
68
145
  );
69
146
 
70
- return { requestLocationPermission, getCurrentLocation };
71
- };
147
+ /**
148
+ * Stop Bluetooth scan
149
+ */
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;
156
+ }
157
+ }, []);
158
+
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
+ /**
266
+ * Calculate distance between coordinates using Haversine formula with altitude
267
+ */
268
+ const calculateEnhancedDistance = useCallback((lat1, lon1, lat2, lon2, alt1 = 0, alt2 = 0) => {
269
+ const R = 6371e3; // Earth's radius in meters
270
+
271
+ const φ1 = lat1 * Math.PI/180;
272
+ const φ2 = lat2 * Math.PI/180;
273
+ const Δφ = (lat2-lat1) * Math.PI/180;
274
+ const Δλ = (lon2-lon1) * Math.PI/180;
275
+
276
+ // Haversine formula for surface distance
277
+ const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
278
+ Math.cos(φ1) * Math.cos(φ2) *
279
+ Math.sin(Δλ/2) * Math.sin(Δλ/2);
280
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
281
+ const surfaceDistance = R * c;
282
+
283
+ // Add altitude difference (Pythagorean theorem)
284
+ const altitudeDiff = Math.abs(alt1 - alt2);
285
+ const totalDistance = Math.sqrt(Math.pow(surfaceDistance, 2) + Math.pow(altitudeDiff, 2));
286
+
287
+ return totalDistance;
288
+ }, []);
289
+
290
+ return {
291
+ requestLocationPermission: requestPermissions,
292
+ getCurrentLocation,
293
+ startBluetoothScan,
294
+ stopBluetoothScan,
295
+ nearbyDevices,
296
+ calculateEnhancedDistance,
297
+ filterStaleDevices,
298
+ };
299
+ };