react-native-biometric-verifier 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -3,8 +3,7 @@ import React, {
3
3
  useEffect,
4
4
  useRef,
5
5
  useCallback,
6
- useImperativeHandle,
7
- forwardRef,
6
+ useMemo,
8
7
  } from "react";
9
8
  import {
10
9
  View,
@@ -17,8 +16,7 @@ import {
17
16
  Animated,
18
17
  } from "react-native";
19
18
  import Icon from "react-native-vector-icons/MaterialIcons";
20
-
21
- // Custom hooks - Removed unnecessary ones
19
+ // Custom hooks
22
20
  import { useCountdown } from "./hooks/useCountdown";
23
21
  import { useGeolocation } from "./hooks/useGeolocation";
24
22
  import { useImageProcessing } from "./hooks/useImageProcessing";
@@ -26,6 +24,7 @@ import { useNotifyMessage } from "./hooks/useNotifyMessage";
26
24
  import { useSafeCallback } from "./hooks/useSafeCallback";
27
25
 
28
26
  // Utils
27
+ import { getDistanceInMeters } from "./utils/distanceCalculator";
29
28
  import { Global } from "./utils/Global";
30
29
  import networkServiceCall from "./utils/NetworkServiceCall";
31
30
  import { getLoaderGif } from "./utils/getLoaderGif";
@@ -38,629 +37,511 @@ import { Notification } from "./components/Notification";
38
37
  import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
39
38
  import StepIndicator from "./components/StepIndicator";
40
39
 
41
- const BiometricModal = forwardRef(({
42
- data,
43
- depkey,
44
- qrscan = false,
45
- callback,
46
- apiurl,
47
- onclose,
48
- frameProcessorFps,
49
- livenessLevel,
50
- fileurl,
51
- imageurl,
52
- navigation,
53
- MaxDistanceMeters = 30,
54
- duration = 100,
55
- antispooflevel,
56
- }, ref) => {
57
- // Custom hooks - Initialize notification hook first
58
- const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
59
- const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
60
-
61
- // Only keep geolocation hook
62
- const {
63
- requestLocationPermission,
64
- getCurrentLocation,
65
- stopLocationWatching,
66
- calculateSafeDistance,
67
- } = useGeolocation(notifyMessage);
68
-
69
- const { convertImageToBase64 } = useImageProcessing();
70
- const safeCallback = useSafeCallback(callback, notifyMessage);
71
-
72
- // State - Simplified
73
- const [modalVisible, setModalVisible] = useState(false);
74
- const [cameraType, setCameraType] = useState("back");
75
- const [state, setState] = useState({
76
- isLoading: false,
77
- loadingType: Global.LoadingTypes.none,
78
- currentStep: "Start",
79
- employeeData: null,
80
- animationState: Global.AnimationStates.qrScan,
81
- qrData: null,
82
- // Removed: wifiReferenceScan
83
- });
84
-
85
- // Refs
86
- const dataRef = useRef(data);
87
- const mountedRef = useRef(true);
88
- const responseRef = useRef(null);
89
- const processedRef = useRef(false);
90
- const resetTimeoutRef = useRef(null);
91
- // Removed: bleScanTimeoutRef, wifiScanTimeoutRef
92
-
93
- // Animation values
94
- const iconScaleAnim = useRef(new Animated.Value(1)).current;
95
- const iconOpacityAnim = useRef(new Animated.Value(0)).current;
96
-
97
- // Expose methods to parent via ref
98
- useImperativeHandle(ref, () => ({
99
- reset: resetState,
100
- start: startProcess,
101
- close: resetState,
102
- getStatus: () => state,
103
- }));
104
-
105
- // Cleanup on unmount - Simplified
106
- useEffect(() => {
107
- return () => {
108
- mountedRef.current = false;
40
+ const BiometricModal = React.memo(
41
+ ({ data, depKey, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, fileurl, imageurl, navigation, duration = 100, MaxDistanceMeters = 30, antispooflevel }) => {
109
42
 
110
- if (resetTimeoutRef.current) {
111
- clearTimeout(resetTimeoutRef.current);
112
- }
43
+ // Custom hooks
44
+ const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
45
+ const { requestLocationPermission, getCurrentLocation } = useGeolocation();
46
+ const { convertImageToBase64 } = useImageProcessing();
47
+ const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
48
+ const safeCallback = useSafeCallback(callback, notifyMessage);
113
49
 
114
- stopLocationWatching();
115
- clearNotification();
116
- };
117
- }, [stopLocationWatching, clearNotification]);
118
-
119
- // Update dataRef when data changes
120
- useEffect(() => {
121
- dataRef.current = data;
122
- }, [data]);
123
-
124
- // Animation helper
125
- const animateIcon = useCallback(() => {
126
- iconScaleAnim.setValue(1);
127
- iconOpacityAnim.setValue(0);
128
-
129
- Animated.sequence([
130
- Animated.parallel([
131
- Animated.timing(iconOpacityAnim, {
132
- toValue: 1,
133
- duration: 300,
134
- useNativeDriver: true,
135
- }),
50
+ // State
51
+ const [modalVisible, setModalVisible] = useState(false);
52
+ const [cameraType, setCameraType] = useState("front");
53
+ const [state, setState] = useState({
54
+ isLoading: false,
55
+ loadingType: Global.LoadingTypes.none,
56
+ currentStep: "Start",
57
+ employeeData: null,
58
+ animationState: Global.AnimationStates.faceScan,
59
+ });
60
+
61
+ // Refs
62
+ const dataRef = useRef(data);
63
+ const mountedRef = useRef(true);
64
+ const responseRef = useRef(null);
65
+ const processedRef = useRef(false);
66
+ const resetTimeoutRef = useRef(null);
67
+
68
+ // Animation values
69
+ const iconScaleAnim = useRef(new Animated.Value(1)).current;
70
+ const iconOpacityAnim = useRef(new Animated.Value(0)).current;
71
+
72
+ // Cleanup on unmount
73
+ useEffect(() => {
74
+ return () => {
75
+ mountedRef.current = false;
76
+
77
+ if (resetTimeoutRef.current) {
78
+ clearTimeout(resetTimeoutRef.current);
79
+ }
80
+
81
+ clearNotification();
82
+ };
83
+ }, []);
84
+
85
+ // Update dataRef when data changes
86
+ useEffect(() => {
87
+ dataRef.current = data;
88
+ }, [data]);
89
+
90
+ // Animation helper
91
+ const animateIcon = useCallback(() => {
92
+ // Reset animation
93
+ iconScaleAnim.setValue(1);
94
+ iconOpacityAnim.setValue(0);
95
+
96
+ // Start animation sequence
97
+ Animated.sequence([
98
+ Animated.parallel([
99
+ Animated.timing(iconOpacityAnim, {
100
+ toValue: 1,
101
+ duration: 300,
102
+ useNativeDriver: true,
103
+ }),
104
+ Animated.spring(iconScaleAnim, {
105
+ toValue: 1.2,
106
+ friction: 3,
107
+ useNativeDriver: true,
108
+ }),
109
+ ]),
136
110
  Animated.spring(iconScaleAnim, {
137
- toValue: 1.2,
138
- friction: 3,
111
+ toValue: 1,
112
+ friction: 5,
139
113
  useNativeDriver: true,
140
114
  }),
141
- ]),
142
- Animated.spring(iconScaleAnim, {
143
- toValue: 1,
144
- friction: 5,
145
- useNativeDriver: true,
146
- }),
147
- ]).start();
148
- }, [iconScaleAnim, iconOpacityAnim]);
149
-
150
- // State update helper
151
- const updateState = useCallback((newState) => {
152
- if (mountedRef.current) {
153
- setState((prev) => {
154
- const merged = { ...prev, ...newState };
155
-
156
- if (JSON.stringify(prev) !== JSON.stringify(merged)) {
157
- if (newState.isLoading !== undefined) {
158
- if (newState.isLoading) {
159
- pauseCountdown();
160
- } else {
161
- resumeCountdown();
115
+ ]).start();
116
+ }, [iconScaleAnim, iconOpacityAnim]);
117
+
118
+ // State update helper
119
+ const updateState = useCallback((newState) => {
120
+ if (mountedRef.current) {
121
+ setState((prev) => {
122
+ const merged = { ...prev, ...newState };
123
+
124
+ if (JSON.stringify(prev) !== JSON.stringify(merged)) {
125
+ // Pause/resume countdown based on loading state
126
+ if (newState.isLoading !== undefined) {
127
+ if (newState.isLoading) {
128
+ pauseCountdown();
129
+ } else {
130
+ resumeCountdown();
131
+ }
162
132
  }
163
- }
164
133
 
165
- if (newState.currentStep && newState.currentStep !== prev.currentStep) {
166
- animateIcon();
167
- }
134
+ // Animate icon when step changes
135
+ if (newState.currentStep && newState.currentStep !== prev.currentStep) {
136
+ animateIcon();
137
+ }
168
138
 
169
- return merged;
170
- }
139
+ return merged;
140
+ }
171
141
 
172
- return prev;
173
- });
174
- }
175
- }, [animateIcon, pauseCountdown, resumeCountdown]);
142
+ return prev;
143
+ });
144
+ }
145
+ }, [animateIcon, pauseCountdown, resumeCountdown]);
176
146
 
177
- // Reset state helper - Simplified
178
- const resetState = useCallback(() => {
179
- if (onclose) {
147
+ // Reset state helper
148
+ const resetState = useCallback(() => {
180
149
  onclose(false);
181
- }
182
150
 
183
- setState({
184
- isLoading: false,
185
- loadingType: Global.LoadingTypes.none,
186
- currentStep: "Start",
187
- employeeData: null,
188
- animationState: Global.AnimationStates.qrScan,
189
- qrData: null,
190
- });
191
-
192
- setModalVisible(false);
193
- processedRef.current = false;
194
- resetCountdown();
195
- stopLocationWatching();
196
- clearNotification();
197
-
198
- if (resetTimeoutRef.current) {
199
- clearTimeout(resetTimeoutRef.current);
200
- resetTimeoutRef.current = null;
201
- }
202
- }, [resetCountdown, stopLocationWatching, clearNotification, onclose]);
203
-
204
- // Error handler
205
- const handleProcessError = useCallback(
206
- (message, errorObj = null) => {
207
- if (errorObj) {
208
- console.error("Process Error:", errorObj);
209
- }
210
-
211
- notifyMessage(message, "error");
212
- updateState({
213
- animationState: Global.AnimationStates.error,
151
+ setState({
214
152
  isLoading: false,
215
153
  loadingType: Global.LoadingTypes.none,
154
+ currentStep: "Start",
155
+ employeeData: null,
156
+ animationState: Global.AnimationStates.faceScan,
216
157
  });
217
158
 
159
+ setModalVisible(false);
160
+ processedRef.current = false;
161
+ resetCountdown();
162
+ clearNotification();
163
+
218
164
  if (resetTimeoutRef.current) {
219
165
  clearTimeout(resetTimeoutRef.current);
166
+ resetTimeoutRef.current = null;
220
167
  }
168
+ }, [resetCountdown, clearNotification]);
221
169
 
222
- resetTimeoutRef.current = setTimeout(() => {
223
- resetState();
224
- }, 1200);
225
- },
226
- [notifyMessage, resetState, updateState]
227
- );
228
-
229
- // Countdown finish handler
230
- const handleCountdownFinish = useCallback(() => {
231
- handleProcessError("Time is up! Please try again.");
232
-
233
- if (navigation?.canGoBack?.()) {
234
- navigation.goBack();
235
- }
236
- }, [handleProcessError, navigation]);
237
-
238
- // API URL validation
239
- const validateApiUrl = useCallback(() => {
240
- if (!apiurl || typeof apiurl !== "string") {
241
- handleProcessError("Invalid API URL configuration.");
242
- return false;
243
- }
244
-
245
- return true;
246
- }, [apiurl, handleProcessError]);
247
-
248
- // Simplified QR scanning handler - GPS + Key verification only
249
- const handleQRScanned = useCallback(
250
- async (qrCodeData) => {
251
- if (!validateApiUrl()) return;
252
-
253
- updateState({
254
- animationState: Global.AnimationStates.processing,
255
- isLoading: true,
256
- loadingType: Global.LoadingTypes.locationVerification,
257
- });
258
-
259
- try {
260
- // 1. Request location permission
261
- updateState({ loadingType: Global.LoadingTypes.locationPermission });
262
- const hasLocationPermission = await requestLocationPermission();
263
- if (!hasLocationPermission) {
264
- handleProcessError("Location permission not granted.");
265
- return;
266
- }
267
-
268
- // 2. Parse QR data with validation
269
- const qrString = typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
270
- if (!qrString || typeof qrString !== "string") {
271
- handleProcessError("Invalid QR code. Please try again.");
272
- return;
170
+ // Error handler
171
+ const handleProcessError = useCallback(
172
+ (message, errorObj = null) => {
173
+ if (errorObj) {
174
+ console.error("Process Error:", errorObj);
273
175
  }
274
176
 
275
- // 3. Parse and validate QR coordinates
276
- const parts = qrString.split(",");
177
+ notifyMessage(message, "error");
178
+ updateState({
179
+ animationState: Global.AnimationStates.error,
180
+ isLoading: false,
181
+ loadingType: Global.LoadingTypes.none,
182
+ });
277
183
 
278
- if (parts.length < 3) {
279
- handleProcessError("Invalid QR format. Expected: latitude,longitude,key");
280
- return;
184
+ if (resetTimeoutRef.current) {
185
+ clearTimeout(resetTimeoutRef.current);
281
186
  }
282
187
 
283
- const latStr = parts[0];
284
- const lngStr = parts[1];
285
- const qrDepKey = parts[2];
286
- const qrAccuracy = parts.length > 3 ? parseFloat(parts[3]) : 5;
188
+ resetTimeoutRef.current = setTimeout(() => {
189
+ resetState();
190
+ }, 1200);
191
+ },
192
+ [notifyMessage, resetState, updateState]
193
+ );
287
194
 
288
- const qrLat = parseFloat(latStr);
289
- const qrLng = parseFloat(lngStr);
195
+ // Countdown finish handler
196
+ const handleCountdownFinish = useCallback(() => {
197
+ handleProcessError("Time is up! Please try again.");
290
198
 
291
- if (isNaN(qrLat) || isNaN(qrLng)) {
292
- handleProcessError("Invalid coordinates in QR code.");
293
- return;
294
- }
199
+ if (navigation.canGoBack()) {
200
+ navigation.goBack();
201
+ }
202
+ }, [handleProcessError, navigation]);
295
203
 
296
- // 4. Get high-accuracy location
297
- updateState({ loadingType: Global.LoadingTypes.gettingLocation });
204
+ // API URL validation
205
+ const validateApiUrl = useCallback(() => {
206
+ if (!apiurl || typeof apiurl !== "string") {
207
+ handleProcessError("Invalid API URL configuration.");
208
+ return false;
209
+ }
298
210
 
299
- let location;
300
- try {
301
- location = await getCurrentLocation();
302
- } catch (locationError) {
303
- console.error('Location error:', locationError);
304
- handleProcessError("Failed to get location. Please try again.");
305
- return;
306
- }
211
+ return true;
212
+ }, [apiurl, handleProcessError]);
307
213
 
308
- // Validate location object
309
- if (!location || typeof location !== 'object') {
310
- handleProcessError("Invalid location data received.");
311
- return;
312
- }
214
+ // Face scan upload
215
+ const uploadFaceScan = useCallback(
216
+ async (selfie) => {
217
+ if (!validateApiUrl()) return;
218
+ const currentData = dataRef.current;
313
219
 
314
- // Validate location coordinates
315
- if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number' ||
316
- isNaN(location.latitude) || isNaN(location.longitude)) {
317
- handleProcessError("Invalid GPS coordinates received.");
220
+ if (!currentData) {
221
+ handleProcessError("Employee data not found.");
318
222
  return;
319
223
  }
320
224
 
321
- // Validate location accuracy
322
- if (location.accuracy > 50) {
323
- notifyMessage(`Location accuracy is ${location.accuracy.toFixed(1)}m. For best results, move to an open area.`, 'warning');
324
- }
325
-
326
- // 5. Calculate distance using safe calculation
327
- updateState({ loadingType: Global.LoadingTypes.calculateDistance });
328
-
329
- const distance = calculateSafeDistance(
330
- { latitude: qrLat, longitude: qrLng },
331
- { latitude: location.latitude, longitude: location.longitude }
332
- );
225
+ updateState({
226
+ isLoading: true,
227
+ loadingType: Global.LoadingTypes.faceRecognition,
228
+ animationState: Global.AnimationStates.processing,
229
+ });
333
230
 
334
- // Validate distance calculation
335
- if (distance === Infinity || isNaN(distance)) {
336
- handleProcessError("Failed to calculate distance. Please try again.");
337
- return;
338
- }
231
+ InteractionManager.runAfterInteractions(async () => {
232
+ let base64;
339
233
 
340
- const distanceWithinThreshold = distance <= MaxDistanceMeters;
341
-
342
- // Simple verification criteria - GPS + Key only
343
- const verificationPassed = distanceWithinThreshold && qrDepKey === depkey;
344
-
345
- if (verificationPassed) {
346
- const locationDetails = {
347
- qrLocation: {
348
- latitude: qrLat,
349
- longitude: qrLng,
350
- accuracy: qrAccuracy
351
- },
352
- deviceLocation: {
353
- latitude: location.latitude,
354
- longitude: location.longitude,
355
- altitude: location.altitude || 0,
356
- accuracy: location.accuracy,
357
- speed: location.speed || 0,
358
- heading: location.heading || 0,
359
- timestamp: location.timestamp || Date.now()
360
- },
361
- distanceMeters: distance,
362
- accuracyBuffer: MaxDistanceMeters,
363
- verified: true,
364
- verificationMethod: "GPS+Key",
365
- verifiedAt: new Date().toISOString(),
366
- locationMethod: location.provider || 'unknown',
367
- qrData: qrString,
368
- qrKey: qrDepKey
369
- };
370
-
371
- responseRef.current = { location: locationDetails };
234
+ try {
235
+ updateState({
236
+ loadingType: Global.LoadingTypes.imageProcessing,
237
+ });
372
238
 
373
- updateState({
374
- qrData: qrString,
375
- animationState: Global.AnimationStates.success,
376
- isLoading: false,
377
- loadingType: Global.LoadingTypes.none,
378
- });
239
+ base64 = await convertImageToBase64(selfie?.uri);
240
+ } catch (err) {
241
+ console.error("Image conversion failed:", err);
242
+ handleProcessError("Image conversion failed.", err);
243
+ return;
244
+ }
379
245
 
380
- const successMessage = `Location verified! Distance: ${distance.toFixed(1)}m (±${Math.max(location.accuracy || 0, qrAccuracy).toFixed(1)}m)`;
381
- notifyMessage(successMessage, "success");
382
- setTimeout(() => startFaceRecognition(), 1200);
383
- } else {
384
- let errorMsg = `Verification failed: ${distance.toFixed(1)}m away`;
385
- if (qrDepKey !== depkey) errorMsg += " (Key mismatch)";
386
- handleProcessError(errorMsg);
387
- }
388
- } catch (error) {
389
- console.error("Location verification failed:", error);
390
- handleProcessError("Unable to verify location. Please try again.", error);
391
- }
392
- },
393
- [
394
- validateApiUrl,
395
- updateState,
396
- requestLocationPermission,
397
- getCurrentLocation,
398
- notifyMessage,
399
- handleProcessError,
400
- startFaceRecognition,
401
- depkey,
402
- MaxDistanceMeters,
403
- calculateSafeDistance,
404
- ]
405
- );
406
-
407
- // Face scan upload - SECOND STEP (simplified, no WiFi revalidation)
408
- const uploadFaceScan = useCallback(
409
- async (selfie) => {
410
- if (!validateApiUrl()) return;
411
-
412
- // Check if QR scan was completed successfully
413
- if (!state.qrData) {
414
- handleProcessError("Please complete QR scan first.");
415
- return;
416
- }
246
+ if (!base64) {
247
+ handleProcessError("Failed to process image.");
248
+ return;
249
+ }
417
250
 
418
- const currentData = dataRef.current;
251
+ try {
252
+ const body = { image: base64 };
253
+ const header = { faceid: currentData };
254
+ const buttonapi = `${apiurl}python/recognize`;
419
255
 
420
- if (!currentData) {
421
- handleProcessError("Employee data not found.");
422
- return;
423
- }
256
+ updateState({
257
+ loadingType: Global.LoadingTypes.networkRequest,
258
+ });
424
259
 
425
- updateState({
426
- isLoading: true,
427
- loadingType: Global.LoadingTypes.faceRecognition,
428
- animationState: Global.AnimationStates.processing,
429
- });
260
+ const response = await networkServiceCall(
261
+ "POST",
262
+ buttonapi,
263
+ header,
264
+ body
265
+ );
430
266
 
431
- InteractionManager.runAfterInteractions(async () => {
432
- let base64;
267
+ if (response?.httpstatus === 200) {
268
+ responseRef.current = {
269
+ ...responseRef.current,
270
+ faceRecognition: response.data?.data || null,
271
+ };
272
+ updateState({
273
+ employeeData: response.data?.data || null,
274
+ animationState: Global.AnimationStates.success,
275
+ isLoading: false,
276
+ loadingType: Global.LoadingTypes.none,
277
+ });
278
+
279
+ notifyMessage("Identity verified successfully!", "success");
280
+
281
+ safeCallback(responseRef.current);
282
+
283
+ if (resetTimeoutRef.current) {
284
+ clearTimeout(resetTimeoutRef.current);
285
+ }
286
+
287
+ resetTimeoutRef.current = setTimeout(() => {
288
+ resetState();
289
+ }, 1200);
290
+ } else {
291
+ handleProcessError(
292
+ response?.data?.message ||
293
+ "Face not recognized. Please try again."
294
+ );
295
+ }
296
+ } catch (error) {
297
+ console.error("Network request failed:", error);
298
+ handleProcessError(
299
+ "Connection error. Please check your network.",
300
+ error
301
+ );
302
+ }
303
+ });
304
+ },
305
+ [
306
+ convertImageToBase64,
307
+ notifyMessage,
308
+ qrscan,
309
+ resetState,
310
+ updateState,
311
+ validateApiUrl,
312
+ safeCallback,
313
+ handleProcessError
314
+ ]
315
+ );
316
+
317
+ // QR code processing
318
+ const handleQRScanned = useCallback(
319
+ async (qrCodeData) => {
320
+ if (!validateApiUrl()) return;
321
+
322
+ updateState({
323
+ animationState: Global.AnimationStates.processing,
324
+ isLoading: true,
325
+ loadingType: Global.LoadingTypes.locationVerification,
326
+ });
433
327
 
434
328
  try {
435
329
  updateState({
436
- loadingType: Global.LoadingTypes.imageProcessing,
330
+ loadingType: Global.LoadingTypes.locationPermission,
437
331
  });
438
332
 
439
- base64 = await convertImageToBase64(selfie?.uri);
440
- } catch (err) {
441
- console.error("Image conversion failed:", err);
442
- handleProcessError("Image conversion failed.", err);
443
- return;
444
- }
333
+ const hasPermission = await requestLocationPermission();
445
334
 
446
- if (!base64) {
447
- handleProcessError("Failed to process image.");
448
- return;
449
- }
335
+ if (!hasPermission) {
336
+ handleProcessError("Location permission not granted.");
337
+ return;
338
+ }
450
339
 
451
- try {
452
- const body = { image: base64 };
453
- const header = { faceid: currentData };
454
- const buttonapi = `${apiurl}python/recognize`;
340
+ const qrString =
341
+ typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
342
+
343
+ if (!qrString || typeof qrString !== "string") {
344
+ handleProcessError("Invalid QR code. Please try again.");
345
+ return;
346
+ }
455
347
 
456
348
  updateState({
457
- loadingType: Global.LoadingTypes.networkRequest,
349
+ loadingType: Global.LoadingTypes.gettingLocation,
458
350
  });
459
351
 
460
- const response = await networkServiceCall(
461
- "POST",
462
- buttonapi,
463
- header,
464
- body
465
- );
352
+ const location = await getCurrentLocation();
466
353
 
467
- if (response?.httpstatus === 200) {
468
- // Combine face recognition response with QR location data
469
- responseRef.current = {
470
- ...responseRef.current,
471
- ...response.data?.data || {},
472
- faceRecognition: response.data?.data || null,
473
- };
354
+ const [latStr, lngStr, qrKey] = qrString.split(",");
355
+ const lat = parseFloat(latStr);
356
+ const lng = parseFloat(lngStr);
357
+ const validCoords = !isNaN(lat) && !isNaN(lng);
358
+ const validDev =
359
+ !isNaN(location?.latitude) && !isNaN(location?.longitude);
474
360
 
361
+ if (validCoords && validDev) {
475
362
  updateState({
476
- employeeData: response.data?.data || null,
477
- animationState: Global.AnimationStates.success,
478
- isLoading: false,
479
- loadingType: Global.LoadingTypes.none,
363
+ loadingType: Global.LoadingTypes.calculateDistance,
480
364
  });
481
365
 
482
- notifyMessage("Identity verified successfully!", "success");
366
+ const distance = getDistanceInMeters(
367
+ lat,
368
+ lng,
369
+ location.latitude,
370
+ location.longitude
371
+ );
483
372
 
484
- // Call the callback with combined data
485
- safeCallback(responseRef.current);
373
+ if (distance <= MaxDistanceMeters && qrKey === depKey) {
374
+ responseRef.current = location
375
+ notifyMessage("Location verified successfully!", "success");
486
376
 
487
- if (resetTimeoutRef.current) {
488
- clearTimeout(resetTimeoutRef.current);
489
- }
377
+ updateState({
378
+ animationState: Global.AnimationStates.success,
379
+ isLoading: false,
380
+ loadingType: Global.LoadingTypes.none,
381
+ });
382
+ setTimeout(() => handleStartFaceScan(), 1200);
490
383
 
491
- resetTimeoutRef.current = setTimeout(() => {
492
- resetState();
493
- }, 1200);
384
+ } else {
385
+ handleProcessError(
386
+ `Location mismatch (${distance.toFixed(0)}m away).`
387
+ );
388
+ }
494
389
  } else {
495
- handleProcessError(
496
- response?.data?.message ||
497
- "Face not recognized. Please try again."
498
- );
390
+ handleProcessError("Invalid coordinates in QR code.");
499
391
  }
500
392
  } catch (error) {
501
- console.error("Network request failed:", error);
393
+ console.error("Location verification failed:", error);
502
394
  handleProcessError(
503
- "Connection error. Please check your network.",
395
+ "Unable to verify location. Please try again.",
504
396
  error
505
397
  );
506
398
  }
399
+ },
400
+ [
401
+ getCurrentLocation,
402
+ notifyMessage,
403
+ requestLocationPermission,
404
+ resetState,
405
+ updateState,
406
+ validateApiUrl,
407
+ handleProcessError
408
+ ]
409
+ );
410
+
411
+ // Image capture handler
412
+ const handleImageCapture = useCallback(
413
+ async (capturedData) => {
414
+ if (state.currentStep === "Identity Verification") {
415
+ uploadFaceScan(capturedData);
416
+ } else if (state.currentStep === "Location Verification") {
417
+ handleQRScanned(capturedData);
418
+ }
419
+ },
420
+ [state.currentStep, uploadFaceScan, handleQRScanned]
421
+ );
422
+
423
+ // Start face scan
424
+ const handleStartFaceScan = useCallback(() => {
425
+ updateState({
426
+ currentStep: "Identity Verification",
427
+ animationState: Global.AnimationStates.faceScan,
507
428
  });
508
- },
509
- [
510
- convertImageToBase64,
511
- notifyMessage,
512
- resetState,
513
- updateState,
514
- validateApiUrl,
515
- safeCallback,
516
- handleProcessError,
517
- state.qrData,
518
- apiurl,
519
- ]
520
- );
521
-
522
- // Image capture handler
523
- const handleImageCapture = useCallback(
524
- async (capturedData) => {
525
- if (state.currentStep === "Location Verification") {
526
- await handleQRScanned(capturedData);
527
- } else if (state.currentStep === "Identity Verification") {
528
- await uploadFaceScan(capturedData);
529
- }
530
- },
531
- [state.currentStep, uploadFaceScan, handleQRScanned]
532
- );
533
-
534
- // Start QR code scan - FIRST STEP
535
- const handleStartQRScan = useCallback(() => {
536
- updateState({
537
- currentStep: "Location Verification",
538
- animationState: Global.AnimationStates.qrScan,
539
- });
540
- setCameraType("back");
541
- }, [updateState]);
429
+ setCameraType("front");
430
+ }, [updateState]);
542
431
 
543
- // Start face recognition - SECOND STEP
544
- const startFaceRecognition = useCallback(() => {
545
- updateState({
546
- currentStep: "Identity Verification",
547
- animationState: Global.AnimationStates.faceScan,
548
- });
549
- setCameraType("front");
550
- }, [updateState]);
551
-
552
- // Start the verification process
553
- const startProcess = useCallback(() => {
554
- startCountdown(handleCountdownFinish);
555
- if (qrscan) {
556
- handleStartQRScan();
557
- } else {
558
- startFaceRecognition();
559
- }
560
- }, [handleCountdownFinish, handleStartQRScan, startCountdown, startFaceRecognition, qrscan]);
561
-
562
- // Open modal when data is received
563
- useEffect(() => {
564
- if (data && !modalVisible && !processedRef.current) {
565
- processedRef.current = true;
566
- setModalVisible(true);
567
- startProcess();
568
- }
569
- }, [data, modalVisible, startProcess]);
570
-
571
- // Determine if camera should be shown
572
- const shouldShowCamera =
573
- (state.currentStep === "Identity Verification" ||
574
- state.currentStep === "Location Verification") &&
575
- state.animationState !== Global.AnimationStates.success &&
576
- state.animationState !== Global.AnimationStates.error;
577
-
578
- return (
579
- <Modal
580
- visible={modalVisible}
581
- animationType="slide"
582
- transparent
583
- onRequestClose={resetState}
584
- statusBarTranslucent
585
- >
586
- <View style={styles.modalContainer}>
587
- {/* Camera component - full screen */}
588
- {shouldShowCamera && !state.isLoading && (
589
- <View style={styles.cameraContainer}>
590
- <CaptureImageWithoutEdit
591
- cameraType={cameraType}
592
- onCapture={handleImageCapture}
593
- showCodeScanner={state.currentStep === "Location Verification"}
594
- isLoading={state.isLoading}
595
- frameProcessorFps={frameProcessorFps}
596
- livenessLevel={livenessLevel}
597
- antispooflevel={antispooflevel}
598
- />
599
- </View>
600
- )}
601
-
602
- {/* UI elements positioned absolutely on top of camera */}
603
- <TouchableOpacity
604
- style={styles.closeButton}
605
- onPress={resetState}
606
- accessibilityLabel="Close modal"
607
- hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
608
- >
609
- <Icon name="close" size={24} color={Global.AppTheme.light} />
610
- </TouchableOpacity>
611
-
612
- <View style={styles.topContainer}>
613
- {!shouldShowCamera && (
614
- <View style={styles.headerContainer}>
615
- <View style={styles.titleContainer}>
616
- <Text style={styles.title}>Biometric Verification</Text>
617
- <Text style={styles.subtitle}>{state.currentStep}</Text>
618
- </View>
432
+ // Start QR code scan
433
+ const startQRCodeScan = useCallback(() => {
434
+ updateState({
435
+ currentStep: "Location Verification",
436
+ animationState: Global.AnimationStates.qrScan,
437
+ });
438
+ setCameraType("back");
439
+ }, [updateState]);
440
+
441
+ // Start the verification process
442
+ const startProcess = useCallback(() => {
443
+ startCountdown(handleCountdownFinish);
444
+ if (qrscan) {
445
+ startQRCodeScan()
446
+ } else {
447
+ handleStartFaceScan();
448
+ }
449
+ }, [handleCountdownFinish, handleStartFaceScan, startCountdown, startQRCodeScan]);
450
+
451
+ // Open modal when data is received
452
+ useEffect(() => {
453
+ if (data && !modalVisible && !processedRef.current) {
454
+ processedRef.current = true;
455
+ setModalVisible(true);
456
+ startProcess();
457
+ }
458
+ }, [data, modalVisible, startProcess]);
459
+
460
+ // Determine if camera should be shown
461
+ const shouldShowCamera =
462
+ (state.currentStep === "Identity Verification" ||
463
+ state.currentStep === "Location Verification") &&
464
+ state.animationState !== Global.AnimationStates.success &&
465
+ state.animationState !== Global.AnimationStates.error;
466
+
467
+ return (
468
+ <Modal
469
+ visible={modalVisible}
470
+ animationType="slide"
471
+ transparent
472
+ onRequestClose={resetState}
473
+ statusBarTranslucent
474
+ >
475
+ <View style={styles.modalContainer}>
476
+ {/* Camera component - full screen */}
477
+ {shouldShowCamera && !state.isLoading && (
478
+ <View style={styles.cameraContainer}>
479
+ <CaptureImageWithoutEdit
480
+ cameraType={cameraType}
481
+ onCapture={handleImageCapture}
482
+ showCodeScanner={state.currentStep === "Location Verification"}
483
+ isLoading={state.isLoading}
484
+ frameProcessorFps={frameProcessorFps}
485
+ livenessLevel={livenessLevel}
486
+ antispooflevel={antispooflevel}
487
+ />
619
488
  </View>
620
489
  )}
621
- </View>
622
490
 
623
- <View style={styles.topContainerstep}>
624
- <StepIndicator
625
- currentStep={state.currentStep}
626
- qrscan={qrscan}
627
- />
628
- </View>
491
+ {/* UI elements positioned absolutely on top of camera */}
492
+ <TouchableOpacity
493
+ style={styles.closeButton}
494
+ onPress={resetState}
495
+ accessibilityLabel="Close modal"
496
+ hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
497
+ >
498
+ <Icon name="close" size={24} color={Global.AppTheme.light} />
499
+ </TouchableOpacity>
500
+
501
+ <View style={styles.topContainer}>
502
+ {!shouldShowCamera && (
503
+ <View style={styles.headerContainer}>
504
+ <View style={styles.titleContainer}>
505
+ <Text style={styles.title}>Biometric Verification</Text>
506
+ <Text style={styles.subtitle}>{state.currentStep}</Text>
507
+ </View>
508
+ </View>
509
+ )}
510
+ </View>
629
511
 
630
- {state.employeeData && (
631
- <View style={styles.cardContainer}>
632
- <Card employeeData={state.employeeData} apiurl={apiurl} fileurl={fileurl} />
512
+ <View style={styles.topContainerstep}>
513
+ <StepIndicator currentStep={state.currentStep} qrscan={qrscan} />
633
514
  </View>
634
- )}
635
515
 
636
- <View style={styles.notificationContainer}>
637
- <Notification
638
- notification={notification}
639
- fadeAnim={fadeAnim}
640
- slideAnim={slideAnim}
641
- />
642
- </View>
516
+ {state.employeeData && (
517
+ <View style={styles.cardContainer}>
518
+ <Card employeeData={state.employeeData} apiurl={apiurl} fileurl={fileurl} />
519
+ </View>
520
+ )}
521
+
522
+ <View style={styles.notificationContainer}>
523
+ <Notification
524
+ notification={notification}
525
+ fadeAnim={fadeAnim}
526
+ slideAnim={slideAnim}
527
+ />
528
+ </View>
643
529
 
644
- <View style={styles.timerContainer}>
645
- <CountdownTimer
646
- duration={duration}
647
- currentTime={countdown}
530
+ <View style={styles.timerContainer}>
531
+ <CountdownTimer
532
+ duration={Global.CountdownDuration}
533
+ currentTime={countdown}
534
+ />
535
+ </View>
536
+ <Loader
537
+ state={state}
538
+ gifSource={getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl)}
648
539
  />
649
540
  </View>
650
- <Loader
651
- state={state}
652
- gifSource={getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl)}
653
- />
654
- </View>
655
- </Modal>
656
- );
657
- });
658
-
659
- // Wrap with memo after forwardRef
660
- const MemoizedBiometricModal = React.memo(BiometricModal);
661
-
662
- // Add display name for debugging
663
- MemoizedBiometricModal.displayName = 'BiometricModal';
541
+ </Modal>
542
+ );
543
+ }
544
+ );
664
545
 
665
546
  const styles = StyleSheet.create({
666
547
  modalContainer: {
@@ -687,7 +568,6 @@ const styles = StyleSheet.create({
687
568
  position: 'absolute',
688
569
  bottom: Platform.OS === 'ios' ? 50 : 30,
689
570
  left: 0,
690
- right: 0,
691
571
  zIndex: 10,
692
572
  },
693
573
  headerContainer: {
@@ -723,16 +603,6 @@ const styles = StyleSheet.create({
723
603
  textShadowOffset: { width: 1, height: 1 },
724
604
  textShadowRadius: 2,
725
605
  },
726
- wifiIndicator: {
727
- fontSize: 12,
728
- color: Global.AppTheme.primary || '#4CAF50',
729
- marginTop: 4,
730
- fontWeight: '500',
731
- fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
732
- textShadowColor: 'rgba(0, 0, 0, 0.2)',
733
- textShadowOffset: { width: 1, height: 1 },
734
- textShadowRadius: 1,
735
- },
736
606
  closeButton: {
737
607
  position: 'absolute',
738
608
  top: Platform.OS === 'ios' ? 40 : 20,
@@ -767,4 +637,4 @@ const styles = StyleSheet.create({
767
637
  },
768
638
  });
769
639
 
770
- export default MemoizedBiometricModal;
640
+ export default BiometricModal;