react-native-biometric-verifier 0.0.15 → 0.0.16
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/components/CaptureImageWithoutEdit.js +182 -214
- package/src/components/{CCard.js → Card.js} +4 -4
- package/src/hooks/useFaceDetectionFrameProcessor.js +167 -0
- package/src/index.js +162 -64
- package/src/utils/NetworkServiceCall.js +11 -1
- package/src/utils/logger.js +0 -7
package/package.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// CaptureImageWithoutEdit.js
|
|
2
1
|
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
3
2
|
import {
|
|
4
3
|
View,
|
|
@@ -16,19 +15,10 @@ import {
|
|
|
16
15
|
getCameraDevice,
|
|
17
16
|
useCodeScanner,
|
|
18
17
|
useCameraFormat,
|
|
19
|
-
useFrameProcessor,
|
|
20
18
|
CameraRuntimeError,
|
|
21
19
|
} from 'react-native-vision-camera';
|
|
22
|
-
import { Worklets } from 'react-native-worklets-core';
|
|
23
|
-
import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
24
20
|
import { COLORS } from "../utils/constants";
|
|
25
|
-
|
|
26
|
-
// Constants for configuration
|
|
27
|
-
const FACE_STABILITY_THRESHOLD = 4; // how many stable samples required
|
|
28
|
-
const FACE_MOVEMENT_THRESHOLD = 18; // pixel threshold (adjust as needed)
|
|
29
|
-
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 700; // throttle; process at most every ~700ms
|
|
30
|
-
const FRAME_PROCESSOR_FPS = 1;
|
|
31
|
-
const MIN_FACE_SIZE = 0.2;
|
|
21
|
+
import { useFaceDetectionFrameProcessor } from '../hooks/useFaceDetectionFrameProcessor';
|
|
32
22
|
|
|
33
23
|
const CaptureImageWithoutEdit = React.memo(
|
|
34
24
|
({
|
|
@@ -38,6 +28,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
38
28
|
showCodeScanner = false,
|
|
39
29
|
isLoading = false,
|
|
40
30
|
currentStep = '',
|
|
31
|
+
frameProcessorFps = 1,
|
|
41
32
|
}) => {
|
|
42
33
|
const cameraRef = useRef(null);
|
|
43
34
|
const [cameraDevice, setCameraDevice] = useState(null);
|
|
@@ -45,65 +36,48 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
45
36
|
const [showCamera, setShowCamera] = useState(false);
|
|
46
37
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
47
38
|
|
|
48
|
-
const [faces, setFaces] = useState([]);
|
|
39
|
+
const [faces, setFaces] = useState([]);
|
|
49
40
|
const [singleFaceDetected, setSingleFaceDetected] = useState(false);
|
|
50
41
|
const [cameraError, setCameraError] = useState(null);
|
|
51
42
|
|
|
52
|
-
const captured = useRef(false);
|
|
43
|
+
const captured = useRef(false);
|
|
53
44
|
const appState = useRef(AppState.currentState);
|
|
54
45
|
const isMounted = useRef(true);
|
|
46
|
+
const initializationAttempts = useRef(0);
|
|
47
|
+
const maxInitializationAttempts = 3;
|
|
48
|
+
|
|
49
|
+
// Reset capture state
|
|
50
|
+
const resetCaptureState = useCallback(() => {
|
|
51
|
+
captured.current = false;
|
|
52
|
+
setSingleFaceDetected(false);
|
|
53
|
+
setFaces([]);
|
|
54
|
+
}, []);
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
const faceDetectionOptions = {
|
|
58
|
-
performanceMode: 'fast',
|
|
59
|
-
landmarkMode: 'none',
|
|
60
|
-
contourMode: 'none',
|
|
61
|
-
classificationMode: 'none',
|
|
62
|
-
minFaceSize: MIN_FACE_SIZE,
|
|
63
|
-
};
|
|
64
|
-
const { detectFaces } = useFaceDetector(faceDetectionOptions);
|
|
65
|
-
|
|
66
|
-
// Code scanner (unchanged)
|
|
56
|
+
// Code scanner
|
|
67
57
|
const codeScanner = useCodeScanner({
|
|
68
58
|
codeTypes: ['qr', 'ean-13'],
|
|
69
59
|
onCodeScanned: (codes) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
try {
|
|
61
|
+
if (showCodeScanner && codes && codes[0]?.value && !isLoading) {
|
|
62
|
+
console.log('QR Code scanned:', codes[0].value);
|
|
63
|
+
onCapture(codes[0].value);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error processing scanned code:', error);
|
|
67
|
+
setCameraError('Failed to process QR code');
|
|
73
68
|
}
|
|
74
69
|
},
|
|
75
70
|
});
|
|
76
71
|
|
|
77
|
-
//
|
|
78
|
-
// --- Worklet-safe shared state (create once) ---
|
|
79
|
-
//
|
|
80
|
-
// Use useMemo to create shared values only once per component lifetime.
|
|
81
|
-
const lastProcessedTime = useMemo(() => Worklets.createSharedValue(0), []);
|
|
82
|
-
const lastBounds = useMemo(() => Worklets.createSharedValue(null), []);
|
|
83
|
-
const stableCount = useMemo(() => Worklets.createSharedValue(0), []);
|
|
84
|
-
const capturedSV = useMemo(() => Worklets.createSharedValue(false), []);
|
|
85
|
-
const showCodeScannerSV = useMemo(() => Worklets.createSharedValue(false), []);
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
// --- RunOnJS callbacks (small payloads only) ---
|
|
89
|
-
//
|
|
72
|
+
// Callbacks for face detection events
|
|
90
73
|
const onStableFaceDetected = useCallback((faceRect) => {
|
|
91
74
|
if (!isMounted.current) return;
|
|
92
75
|
if (captured.current) return;
|
|
93
76
|
|
|
94
|
-
// keep JS-side guard
|
|
95
77
|
captured.current = true;
|
|
96
|
-
// keep UI minimal
|
|
97
78
|
setSingleFaceDetected(true);
|
|
98
79
|
setFaces([faceRect]);
|
|
99
80
|
|
|
100
|
-
// Ensure the worklet shared flag is set (worklet already sets it but keep JS consistent)
|
|
101
|
-
try {
|
|
102
|
-
capturedSV.value = true;
|
|
103
|
-
} catch (e) {
|
|
104
|
-
// ignore if not accessible for some reason
|
|
105
|
-
}
|
|
106
|
-
|
|
107
81
|
(async () => {
|
|
108
82
|
try {
|
|
109
83
|
if (!cameraRef.current) {
|
|
@@ -117,6 +91,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
117
91
|
skipMetadata: true,
|
|
118
92
|
});
|
|
119
93
|
|
|
94
|
+
if (!photo || !photo.path) {
|
|
95
|
+
throw new Error('Failed to capture photo - no path returned');
|
|
96
|
+
}
|
|
97
|
+
|
|
120
98
|
const photopath = `file://${photo.path}`;
|
|
121
99
|
const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
|
|
122
100
|
const photoData = {
|
|
@@ -128,169 +106,146 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
128
106
|
onCapture(photoData);
|
|
129
107
|
} catch (e) {
|
|
130
108
|
console.error('Capture error:', e);
|
|
131
|
-
// reset both JS and worklet flags so user can retry
|
|
132
109
|
captured.current = false;
|
|
133
|
-
|
|
134
|
-
capturedSV.value = false;
|
|
135
|
-
stableCount.value = 0;
|
|
136
|
-
lastBounds.value = null;
|
|
137
|
-
} catch (err) {
|
|
138
|
-
// swallow
|
|
139
|
-
}
|
|
110
|
+
resetCaptureState();
|
|
140
111
|
setCameraError('Failed to capture image. Please try again.');
|
|
141
112
|
}
|
|
142
113
|
})();
|
|
143
|
-
}, [onCapture,
|
|
114
|
+
}, [onCapture, resetCaptureState]);
|
|
144
115
|
|
|
145
116
|
const onFacesUpdate = useCallback((payload) => {
|
|
146
117
|
if (!isMounted.current) return;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
118
|
+
try {
|
|
119
|
+
const { count } = payload;
|
|
120
|
+
if (count === 1) {
|
|
121
|
+
setSingleFaceDetected(true);
|
|
122
|
+
setFaces(prev => {
|
|
123
|
+
if (prev.length === 1) return prev;
|
|
124
|
+
return [{ x: 0, y: 0, width: 0, height: 0 }];
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
setSingleFaceDetected(false);
|
|
128
|
+
setFaces([]);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error updating faces:', error);
|
|
157
132
|
}
|
|
158
|
-
// we don't store 'progress' in state to keep UI lightweight
|
|
159
133
|
}, []);
|
|
160
134
|
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (!lastProcessedTime.value) lastProcessedTime.value = 0;
|
|
174
|
-
if (now - lastProcessedTime.value < FRAME_PROCESSOR_MIN_INTERVAL_MS) return;
|
|
175
|
-
lastProcessedTime.value = now;
|
|
135
|
+
// Use the face detection frame processor hook
|
|
136
|
+
const {
|
|
137
|
+
frameProcessor,
|
|
138
|
+
resetCaptureState: resetFrameProcessor,
|
|
139
|
+
updateShowCodeScanner,
|
|
140
|
+
capturedSV,
|
|
141
|
+
} = useFaceDetectionFrameProcessor({
|
|
142
|
+
onStableFaceDetected,
|
|
143
|
+
onFacesUpdate,
|
|
144
|
+
showCodeScanner,
|
|
145
|
+
isLoading,
|
|
146
|
+
});
|
|
176
147
|
|
|
148
|
+
// Initialize camera
|
|
149
|
+
const initializeCamera = useCallback(async () => {
|
|
177
150
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
151
|
+
if (!isMounted.current) return;
|
|
152
|
+
if (initializationAttempts.current >= maxInitializationAttempts) {
|
|
153
|
+
setCameraError('Failed to initialize camera after multiple attempts');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
180
156
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const bounds = f.bounds; // { x, y, width, height }
|
|
157
|
+
initializationAttempts.current += 1;
|
|
158
|
+
setCameraError(null);
|
|
184
159
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
stableCount.value = 1;
|
|
188
|
-
} else {
|
|
189
|
-
const dx = Math.abs(bounds.x - lastBounds.value.x);
|
|
190
|
-
const dy = Math.abs(bounds.y - lastBounds.value.y);
|
|
160
|
+
const permission = await Camera.requestCameraPermission();
|
|
161
|
+
setCameraPermission(permission);
|
|
191
162
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
lastBounds.value = bounds;
|
|
163
|
+
if (permission === 'granted') {
|
|
164
|
+
const devices = await Camera.getAvailableCameraDevices();
|
|
165
|
+
|
|
166
|
+
if (!devices || devices.length === 0) {
|
|
167
|
+
throw new Error('No camera devices found');
|
|
198
168
|
}
|
|
169
|
+
|
|
170
|
+
const device = getCameraDevice(devices, cameraType);
|
|
199
171
|
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// send only minimal rectangle data to JS to avoid expensive bridging
|
|
205
|
-
const faceRect = {
|
|
206
|
-
x: Math.round(lastBounds.value.x),
|
|
207
|
-
y: Math.round(lastBounds.value.y),
|
|
208
|
-
width: Math.round(lastBounds.value.width),
|
|
209
|
-
height: Math.round(lastBounds.value.height),
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
runOnStable(faceRect);
|
|
172
|
+
if (device) {
|
|
173
|
+
setCameraDevice(device);
|
|
174
|
+
setTimeout(() => setShowCamera(true), 100);
|
|
175
|
+
initializationAttempts.current = 0; // Reset attempts on success
|
|
213
176
|
} else {
|
|
214
|
-
|
|
215
|
-
runOnFaces({ count: 1, progress: stableCount.value || 0 });
|
|
177
|
+
throw new Error(`No camera device found for type: ${cameraType}`);
|
|
216
178
|
}
|
|
217
179
|
} else {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
180
|
+
throw new Error(`Camera permission ${permission}`);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('Camera initialization failed:', error);
|
|
184
|
+
if (isMounted.current) {
|
|
185
|
+
let errorMessage = 'Failed to initialize camera';
|
|
186
|
+
|
|
187
|
+
if (error.message.includes('permission')) {
|
|
188
|
+
errorMessage = 'Camera permission denied';
|
|
189
|
+
} else if (error.message.includes('No camera devices')) {
|
|
190
|
+
errorMessage = 'No camera available on this device';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setCameraError(errorMessage);
|
|
223
194
|
}
|
|
224
|
-
} catch (e) {
|
|
225
|
-
// swallow errors in worklet; don't do heavy logging here
|
|
226
195
|
}
|
|
227
|
-
}, [
|
|
196
|
+
}, [cameraType]);
|
|
228
197
|
|
|
229
|
-
//
|
|
230
|
-
// --- Effects: lifecycle, camera init, app state, cleanup ---
|
|
231
|
-
//
|
|
198
|
+
// Effects: lifecycle, camera init, app state, cleanup
|
|
232
199
|
useEffect(() => {
|
|
233
200
|
isMounted.current = true;
|
|
234
|
-
|
|
235
|
-
const initializeCamera = async () => {
|
|
236
|
-
try {
|
|
237
|
-
if (!isMounted.current) return;
|
|
238
|
-
|
|
239
|
-
const permission = await Camera.requestCameraPermission();
|
|
240
|
-
setCameraPermission(permission);
|
|
241
|
-
|
|
242
|
-
if (permission === 'granted') {
|
|
243
|
-
const devices = await Camera.getAvailableCameraDevices();
|
|
244
|
-
const device = getCameraDevice(devices, cameraType);
|
|
245
|
-
|
|
246
|
-
if (device) {
|
|
247
|
-
setCameraDevice(device);
|
|
248
|
-
// short delay so camera UI transitions cleanly
|
|
249
|
-
setTimeout(() => setShowCamera(true), 100);
|
|
250
|
-
} else {
|
|
251
|
-
console.error('No camera device found for type:', cameraType);
|
|
252
|
-
setCameraError('Camera not available on this device');
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
setCameraError('Camera permission denied');
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
console.error('Camera init failed:', error);
|
|
259
|
-
if (isMounted.current) {
|
|
260
|
-
setCameraError('Failed to initialize camera');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
201
|
initializeCamera();
|
|
266
202
|
|
|
267
203
|
return () => {
|
|
268
204
|
isMounted.current = false;
|
|
269
205
|
setShowCamera(false);
|
|
270
206
|
};
|
|
271
|
-
}, [
|
|
207
|
+
}, [initializeCamera]);
|
|
272
208
|
|
|
273
209
|
// app state change
|
|
274
210
|
useEffect(() => {
|
|
275
211
|
const subscription = AppState.addEventListener('change', nextAppState => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
212
|
+
try {
|
|
213
|
+
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
214
|
+
if (cameraPermission === 'granted') {
|
|
215
|
+
setShowCamera(true);
|
|
216
|
+
}
|
|
217
|
+
} else if (nextAppState.match(/inactive|background/)) {
|
|
218
|
+
setShowCamera(false);
|
|
280
219
|
}
|
|
281
|
-
|
|
282
|
-
|
|
220
|
+
appState.current = nextAppState;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error handling app state change:', error);
|
|
283
223
|
}
|
|
284
|
-
appState.current = nextAppState;
|
|
285
224
|
});
|
|
286
225
|
|
|
287
|
-
return () =>
|
|
226
|
+
return () => {
|
|
227
|
+
try {
|
|
228
|
+
subscription.remove();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('Error removing app state listener:', error);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
288
233
|
}, [cameraPermission]);
|
|
289
234
|
|
|
290
|
-
// android back handler
|
|
235
|
+
// android back handler
|
|
291
236
|
useEffect(() => {
|
|
292
|
-
const backHandler = BackHandler.addEventListener('hardwareBackPress', () =>
|
|
293
|
-
|
|
237
|
+
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
238
|
+
// Don't allow back press in camera screen
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return () => {
|
|
243
|
+
try {
|
|
244
|
+
backHandler.remove();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Error removing back handler:', error);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
294
249
|
}, []);
|
|
295
250
|
|
|
296
251
|
// handle camera errors
|
|
@@ -323,13 +278,34 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
323
278
|
setCameraError(null);
|
|
324
279
|
}, []);
|
|
325
280
|
|
|
326
|
-
// format selection
|
|
281
|
+
// format selection
|
|
327
282
|
const format = useCameraFormat(cameraDevice, [
|
|
328
283
|
{ videoResolution: { width: 640, height: 640 } },
|
|
329
284
|
{ fps: 30 },
|
|
330
285
|
]);
|
|
331
286
|
|
|
332
|
-
//
|
|
287
|
+
// keep worklet's showCodeScanner flag in sync
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
try {
|
|
290
|
+
updateShowCodeScanner(!!showCodeScanner);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('Error updating code scanner:', error);
|
|
293
|
+
}
|
|
294
|
+
}, [showCodeScanner, updateShowCodeScanner]);
|
|
295
|
+
|
|
296
|
+
// Retry camera initialization
|
|
297
|
+
const handleRetry = useCallback(() => {
|
|
298
|
+
setCameraError(null);
|
|
299
|
+
setShowCamera(false);
|
|
300
|
+
resetCaptureState();
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
if (isMounted.current) {
|
|
303
|
+
initializeCamera();
|
|
304
|
+
}
|
|
305
|
+
}, 500);
|
|
306
|
+
}, [initializeCamera, resetCaptureState]);
|
|
307
|
+
|
|
308
|
+
// camera placeholder / UI
|
|
333
309
|
const renderCameraPlaceholder = () => (
|
|
334
310
|
<View style={styles.cameraContainer}>
|
|
335
311
|
{cameraError ? (
|
|
@@ -338,25 +314,25 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
338
314
|
<Text style={styles.errorText}>{cameraError}</Text>
|
|
339
315
|
<TouchableOpacity
|
|
340
316
|
style={styles.retryButton}
|
|
341
|
-
onPress={
|
|
342
|
-
|
|
343
|
-
setShowCamera(false);
|
|
344
|
-
setTimeout(() => {
|
|
345
|
-
if (isMounted.current) {
|
|
346
|
-
setShowCamera(true);
|
|
347
|
-
}
|
|
348
|
-
}, 500);
|
|
349
|
-
}}
|
|
317
|
+
onPress={handleRetry}
|
|
318
|
+
accessibilityLabel="Retry camera initialization"
|
|
350
319
|
>
|
|
351
320
|
<Text style={styles.retryButtonText}>Retry</Text>
|
|
352
321
|
</TouchableOpacity>
|
|
353
322
|
</View>
|
|
354
|
-
) : cameraPermission === 'denied' ? (
|
|
323
|
+
) : cameraPermission === 'denied' || cameraPermission === 'restricted' ? (
|
|
355
324
|
<View style={styles.placeholderContainer}>
|
|
356
325
|
<Icon name="camera-off" size={40} color={COLORS.light} />
|
|
357
326
|
<Text style={styles.placeholderText}>
|
|
358
327
|
Camera permission required. Please enable in settings.
|
|
359
328
|
</Text>
|
|
329
|
+
<TouchableOpacity
|
|
330
|
+
style={styles.retryButton}
|
|
331
|
+
onPress={handleRetry}
|
|
332
|
+
accessibilityLabel="Request camera permission again"
|
|
333
|
+
>
|
|
334
|
+
<Text style={styles.retryButtonText}>Request Again</Text>
|
|
335
|
+
</TouchableOpacity>
|
|
360
336
|
</View>
|
|
361
337
|
) : cameraPermission === 'not-determined' ? (
|
|
362
338
|
<View style={styles.placeholderContainer}>
|
|
@@ -369,6 +345,13 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
369
345
|
<View style={styles.placeholderContainer}>
|
|
370
346
|
<Icon name="camera-alt" size={40} color={COLORS.light} />
|
|
371
347
|
<Text style={styles.placeholderText}>Camera not available</Text>
|
|
348
|
+
<TouchableOpacity
|
|
349
|
+
style={styles.retryButton}
|
|
350
|
+
onPress={handleRetry}
|
|
351
|
+
accessibilityLabel="Retry camera initialization"
|
|
352
|
+
>
|
|
353
|
+
<Text style={styles.retryButtonText}>Retry</Text>
|
|
354
|
+
</TouchableOpacity>
|
|
372
355
|
</View>
|
|
373
356
|
) : (
|
|
374
357
|
<View style={styles.placeholderContainer}>
|
|
@@ -384,33 +367,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
384
367
|
cameraDevice &&
|
|
385
368
|
!cameraError;
|
|
386
369
|
|
|
387
|
-
// keep worklet's showCodeScanner flag in sync
|
|
388
|
-
useEffect(() => {
|
|
389
|
-
try {
|
|
390
|
-
showCodeScannerSV.value = !!showCodeScanner;
|
|
391
|
-
} catch (e) {
|
|
392
|
-
// ignore
|
|
393
|
-
}
|
|
394
|
-
return () => {
|
|
395
|
-
try { showCodeScannerSV.value = false; } catch (e) {}
|
|
396
|
-
};
|
|
397
|
-
}, [showCodeScanner, showCodeScannerSV]);
|
|
398
|
-
|
|
399
|
-
// cleanup shared values when component unmounts
|
|
400
|
-
useEffect(() => {
|
|
401
|
-
return () => {
|
|
402
|
-
try {
|
|
403
|
-
lastProcessedTime.value = 0;
|
|
404
|
-
lastBounds.value = null;
|
|
405
|
-
stableCount.value = 0;
|
|
406
|
-
capturedSV.value = false;
|
|
407
|
-
showCodeScannerSV.value = false;
|
|
408
|
-
} catch (e) {
|
|
409
|
-
// ignore
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
}, [lastProcessedTime, lastBounds, stableCount, capturedSV, showCodeScannerSV]);
|
|
413
|
-
|
|
414
370
|
return (
|
|
415
371
|
<View style={styles.cameraContainer}>
|
|
416
372
|
{shouldRenderCamera ? (
|
|
@@ -423,7 +379,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
423
379
|
format={format}
|
|
424
380
|
codeScanner={showCodeScanner ? codeScanner : undefined}
|
|
425
381
|
frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
|
|
426
|
-
frameProcessorFps={
|
|
382
|
+
frameProcessorFps={frameProcessorFps}
|
|
427
383
|
onInitialized={handleCameraInitialized}
|
|
428
384
|
onError={handleCameraError}
|
|
429
385
|
enableZoomGesture={false}
|
|
@@ -455,9 +411,12 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
455
411
|
</Text>
|
|
456
412
|
)}
|
|
457
413
|
{captured.current && (
|
|
458
|
-
<
|
|
459
|
-
|
|
460
|
-
|
|
414
|
+
<View style={styles.capturingContainer}>
|
|
415
|
+
<ActivityIndicator size="small" color={COLORS.light} />
|
|
416
|
+
<Text style={[styles.faceDetectionText, styles.capturingText]}>
|
|
417
|
+
Capturing...
|
|
418
|
+
</Text>
|
|
419
|
+
</View>
|
|
461
420
|
)}
|
|
462
421
|
</View>
|
|
463
422
|
)}
|
|
@@ -521,6 +480,7 @@ const styles = StyleSheet.create({
|
|
|
521
480
|
paddingHorizontal: 20,
|
|
522
481
|
paddingVertical: 10,
|
|
523
482
|
borderRadius: 8,
|
|
483
|
+
marginTop: 10,
|
|
524
484
|
},
|
|
525
485
|
retryButtonText: {
|
|
526
486
|
color: COLORS.light,
|
|
@@ -587,6 +547,14 @@ const styles = StyleSheet.create({
|
|
|
587
547
|
capturingText: {
|
|
588
548
|
backgroundColor: 'rgba(0,100,255,0.7)',
|
|
589
549
|
},
|
|
550
|
+
capturingContainer: {
|
|
551
|
+
flexDirection: 'row',
|
|
552
|
+
alignItems: 'center',
|
|
553
|
+
backgroundColor: 'rgba(0,100,255,0.7)',
|
|
554
|
+
paddingHorizontal: 16,
|
|
555
|
+
paddingVertical: 8,
|
|
556
|
+
borderRadius: 8,
|
|
557
|
+
},
|
|
590
558
|
});
|
|
591
559
|
|
|
592
|
-
export default CaptureImageWithoutEdit;
|
|
560
|
+
export default CaptureImageWithoutEdit;
|
|
@@ -4,11 +4,11 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import { COLORS } from '../utils/constants';
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const Card = ({ employeeData, apiurl }) => {
|
|
8
8
|
const [imageError, setImageError] = useState(false);
|
|
9
9
|
|
|
10
10
|
if (!employeeData || typeof employeeData !== 'object') {
|
|
11
|
-
console.warn('
|
|
11
|
+
console.warn('Card: Invalid or missing employeeData');
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -52,7 +52,7 @@ export const CCard = ({ employeeData, apiurl }) => {
|
|
|
52
52
|
);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Card.propTypes = {
|
|
56
56
|
employeeData: PropTypes.shape({
|
|
57
57
|
facename: PropTypes.string,
|
|
58
58
|
faceid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
@@ -133,4 +133,4 @@ const styles = StyleSheet.create({
|
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
export default
|
|
136
|
+
export default Card;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { Worklets } from 'react-native-worklets-core';
|
|
3
|
+
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
4
|
+
import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
5
|
+
|
|
6
|
+
// Tuned constants
|
|
7
|
+
const FACE_STABILITY_THRESHOLD = 3;
|
|
8
|
+
const FACE_MOVEMENT_THRESHOLD = 15;
|
|
9
|
+
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 1000;
|
|
10
|
+
const MIN_FACE_SIZE = 0.2;
|
|
11
|
+
|
|
12
|
+
export const useFaceDetectionFrameProcessor = ({
|
|
13
|
+
onStableFaceDetected = () => {},
|
|
14
|
+
onFacesUpdate = () => {},
|
|
15
|
+
showCodeScanner = false,
|
|
16
|
+
isLoading = false,
|
|
17
|
+
}) => {
|
|
18
|
+
// Face detector (fast mode only)
|
|
19
|
+
const { detectFaces } = useFaceDetector({
|
|
20
|
+
performanceMode: 'fast',
|
|
21
|
+
landmarkMode: 'none',
|
|
22
|
+
contourMode: 'none',
|
|
23
|
+
classificationMode: 'none',
|
|
24
|
+
minFaceSize: MIN_FACE_SIZE,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Shared state
|
|
28
|
+
const lastProcessedTime = useMemo(() => Worklets.createSharedValue(0), []);
|
|
29
|
+
const lastX = useMemo(() => Worklets.createSharedValue(0), []);
|
|
30
|
+
const lastY = useMemo(() => Worklets.createSharedValue(0), []);
|
|
31
|
+
const lastW = useMemo(() => Worklets.createSharedValue(0), []);
|
|
32
|
+
const lastH = useMemo(() => Worklets.createSharedValue(0), []);
|
|
33
|
+
const stableCount = useMemo(() => Worklets.createSharedValue(0), []);
|
|
34
|
+
const capturedSV = useMemo(() => Worklets.createSharedValue(false), []);
|
|
35
|
+
const showCodeScannerSV = useMemo(
|
|
36
|
+
() => Worklets.createSharedValue(showCodeScanner),
|
|
37
|
+
[showCodeScanner]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Safe JS callbacks
|
|
41
|
+
const runOnStable = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
Worklets.createRunOnJS((x, y, width, height) => {
|
|
44
|
+
try {
|
|
45
|
+
onStableFaceDetected?.({
|
|
46
|
+
x: Math.max(0, Math.round(x)),
|
|
47
|
+
y: Math.max(0, Math.round(y)),
|
|
48
|
+
width: Math.max(0, Math.round(width)),
|
|
49
|
+
height: Math.max(0, Math.round(height)),
|
|
50
|
+
});
|
|
51
|
+
} catch {}
|
|
52
|
+
}),
|
|
53
|
+
[onStableFaceDetected]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const runOnFaces = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
Worklets.createRunOnJS((count, progress) => {
|
|
59
|
+
try {
|
|
60
|
+
onFacesUpdate?.({ count, progress });
|
|
61
|
+
} catch {}
|
|
62
|
+
}),
|
|
63
|
+
[onFacesUpdate]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Frame processor
|
|
67
|
+
const frameProcessor = useFrameProcessor((frame) => {
|
|
68
|
+
'worklet';
|
|
69
|
+
|
|
70
|
+
// Kill switches
|
|
71
|
+
if (showCodeScannerSV.value || capturedSV.value || isLoading) return;
|
|
72
|
+
|
|
73
|
+
const now = frame?.timestamp ? frame.timestamp / 1e6 : Date.now();
|
|
74
|
+
if (now - lastProcessedTime.value < FRAME_PROCESSOR_MIN_INTERVAL_MS) return;
|
|
75
|
+
lastProcessedTime.value = now;
|
|
76
|
+
|
|
77
|
+
let detected;
|
|
78
|
+
try {
|
|
79
|
+
detected = detectFaces?.(frame);
|
|
80
|
+
} catch {
|
|
81
|
+
return; // skip invalid frame
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!detected || detected.length === 0) {
|
|
85
|
+
// No faces → reset
|
|
86
|
+
stableCount.value = 0;
|
|
87
|
+
capturedSV.value = false;
|
|
88
|
+
runOnFaces(0, 0);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (detected.length === 1 && !capturedSV.value) {
|
|
93
|
+
const f = detected[0];
|
|
94
|
+
if (!f?.bounds) {
|
|
95
|
+
runOnFaces(0, 0);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Clamp invalid values
|
|
100
|
+
const x = Math.max(0, f.bounds.x ?? 0);
|
|
101
|
+
const y = Math.max(0, f.bounds.y ?? 0);
|
|
102
|
+
const width = Math.max(0, f.bounds.width ?? 0);
|
|
103
|
+
const height = Math.max(0, f.bounds.height ?? 0);
|
|
104
|
+
|
|
105
|
+
if (lastX.value === 0 && lastY.value === 0) {
|
|
106
|
+
// First detection
|
|
107
|
+
lastX.value = x;
|
|
108
|
+
lastY.value = y;
|
|
109
|
+
lastW.value = width;
|
|
110
|
+
lastH.value = height;
|
|
111
|
+
stableCount.value = 1;
|
|
112
|
+
} else {
|
|
113
|
+
const dx = Math.abs(x - lastX.value);
|
|
114
|
+
const dy = Math.abs(y - lastY.value);
|
|
115
|
+
|
|
116
|
+
if (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD) {
|
|
117
|
+
stableCount.value += 1;
|
|
118
|
+
} else {
|
|
119
|
+
stableCount.value = 1; // restart stability
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lastX.value = x;
|
|
123
|
+
lastY.value = y;
|
|
124
|
+
lastW.value = width;
|
|
125
|
+
lastH.value = height;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (stableCount.value >= FACE_STABILITY_THRESHOLD) {
|
|
129
|
+
capturedSV.value = true;
|
|
130
|
+
runOnStable(lastX.value, lastY.value, lastW.value, lastH.value);
|
|
131
|
+
} else {
|
|
132
|
+
runOnFaces(1, stableCount.value);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Multiple faces → not allowed
|
|
136
|
+
stableCount.value = 0;
|
|
137
|
+
capturedSV.value = false;
|
|
138
|
+
runOnFaces(detected.length, 0);
|
|
139
|
+
}
|
|
140
|
+
}, [detectFaces, isLoading]);
|
|
141
|
+
|
|
142
|
+
// Reset everything safely
|
|
143
|
+
const resetCaptureState = useCallback(() => {
|
|
144
|
+
capturedSV.value = false;
|
|
145
|
+
stableCount.value = 0;
|
|
146
|
+
lastX.value = 0;
|
|
147
|
+
lastY.value = 0;
|
|
148
|
+
lastW.value = 0;
|
|
149
|
+
lastH.value = 0;
|
|
150
|
+
lastProcessedTime.value = 0;
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
// Update QR/Scanner toggle
|
|
154
|
+
const updateShowCodeScanner = useCallback(
|
|
155
|
+
(value) => {
|
|
156
|
+
showCodeScannerSV.value = !!value;
|
|
157
|
+
},
|
|
158
|
+
[showCodeScannerSV]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
frameProcessor,
|
|
163
|
+
resetCaptureState,
|
|
164
|
+
updateShowCodeScanner,
|
|
165
|
+
capturedSV,
|
|
166
|
+
};
|
|
167
|
+
};
|
package/src/index.js
CHANGED
|
@@ -16,10 +16,16 @@ import {
|
|
|
16
16
|
Animated,
|
|
17
17
|
} from "react-native";
|
|
18
18
|
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
19
|
+
import { useNavigation } from "@react-navigation/native";
|
|
20
|
+
|
|
21
|
+
// Custom hooks
|
|
19
22
|
import { useCountdown } from "./hooks/useCountdown";
|
|
20
23
|
import { useGeolocation } from "./hooks/useGeolocation";
|
|
21
24
|
import { useImageProcessing } from "./hooks/useImageProcessing";
|
|
22
25
|
import { useNotifyMessage } from "./hooks/useNotifyMessage";
|
|
26
|
+
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
27
|
+
|
|
28
|
+
// Utils
|
|
23
29
|
import { getDistanceInMeters } from "./utils/distanceCalculator";
|
|
24
30
|
import {
|
|
25
31
|
ANIMATION_STATES,
|
|
@@ -28,29 +34,31 @@ import {
|
|
|
28
34
|
MAX_DISTANCE_METERS,
|
|
29
35
|
LOADING_TYPES,
|
|
30
36
|
} from "./utils/constants";
|
|
37
|
+
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
38
|
+
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
39
|
+
|
|
40
|
+
// Components
|
|
31
41
|
import Loader from "./components/Loader";
|
|
32
42
|
import { CountdownTimer } from "./components/CountdownTimer";
|
|
33
|
-
import {
|
|
43
|
+
import { Card } from "./components/Card";
|
|
34
44
|
import { Notification } from "./components/Notification";
|
|
35
|
-
import { useNavigation } from "@react-navigation/native";
|
|
36
|
-
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
37
|
-
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
38
|
-
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
39
45
|
import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
40
46
|
import StepIndicator from "./components/StepIndicator";
|
|
41
47
|
|
|
42
|
-
const
|
|
43
|
-
({ data, qrscan = false, callback, apiurl, onclose }) => {
|
|
48
|
+
const BiometricModal = React.memo(
|
|
49
|
+
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps }) => {
|
|
44
50
|
const navigation = useNavigation();
|
|
51
|
+
|
|
52
|
+
// Custom hooks
|
|
45
53
|
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown();
|
|
46
54
|
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
47
55
|
const { convertImageToBase64 } = useImageProcessing();
|
|
48
|
-
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } =
|
|
49
|
-
|
|
56
|
+
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
57
|
+
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
50
58
|
|
|
59
|
+
// State
|
|
51
60
|
const [modalVisible, setModalVisible] = useState(false);
|
|
52
61
|
const [cameraType, setCameraType] = useState("front");
|
|
53
|
-
|
|
54
62
|
const [state, setState] = useState({
|
|
55
63
|
isLoading: false,
|
|
56
64
|
loadingType: LOADING_TYPES.NONE,
|
|
@@ -59,32 +67,35 @@ const BiometricVerificationModal = React.memo(
|
|
|
59
67
|
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
60
68
|
});
|
|
61
69
|
|
|
70
|
+
// Refs
|
|
62
71
|
const dataRef = useRef(data);
|
|
63
72
|
const mountedRef = useRef(true);
|
|
64
73
|
const responseRef = useRef(null);
|
|
65
74
|
const processedRef = useRef(false);
|
|
66
|
-
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
67
75
|
const resetTimeoutRef = useRef(null);
|
|
68
76
|
|
|
69
|
-
// Animation values
|
|
77
|
+
// Animation values
|
|
70
78
|
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
71
79
|
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
72
80
|
|
|
73
81
|
// Cleanup on unmount
|
|
74
82
|
useEffect(() => {
|
|
75
|
-
console.log("🔧
|
|
83
|
+
console.log("🔧 BiometricModal mounted");
|
|
76
84
|
|
|
77
85
|
return () => {
|
|
78
|
-
console.log("🧹
|
|
86
|
+
console.log("🧹 BiometricModal unmounting - cleaning up");
|
|
79
87
|
mountedRef.current = false;
|
|
88
|
+
|
|
80
89
|
if (resetTimeoutRef.current) {
|
|
81
90
|
clearTimeout(resetTimeoutRef.current);
|
|
82
91
|
console.log("⏹️ Cleared reset timeout");
|
|
83
92
|
}
|
|
93
|
+
|
|
84
94
|
clearNotification();
|
|
85
95
|
};
|
|
86
96
|
}, []);
|
|
87
97
|
|
|
98
|
+
// Update dataRef when data changes
|
|
88
99
|
useEffect(() => {
|
|
89
100
|
dataRef.current = data;
|
|
90
101
|
console.log(
|
|
@@ -93,11 +104,12 @@ const BiometricVerificationModal = React.memo(
|
|
|
93
104
|
);
|
|
94
105
|
}, [data]);
|
|
95
106
|
|
|
107
|
+
// Animation helper
|
|
96
108
|
const animateIcon = useCallback(() => {
|
|
97
109
|
// Reset animation
|
|
98
110
|
iconScaleAnim.setValue(1);
|
|
99
111
|
iconOpacityAnim.setValue(0);
|
|
100
|
-
|
|
112
|
+
|
|
101
113
|
// Start animation sequence
|
|
102
114
|
Animated.sequence([
|
|
103
115
|
Animated.parallel([
|
|
@@ -120,13 +132,15 @@ const BiometricVerificationModal = React.memo(
|
|
|
120
132
|
]).start();
|
|
121
133
|
}, [iconScaleAnim, iconOpacityAnim]);
|
|
122
134
|
|
|
135
|
+
// State update helper
|
|
123
136
|
const updateState = useCallback((newState) => {
|
|
124
137
|
if (mountedRef.current) {
|
|
125
138
|
setState((prev) => {
|
|
126
139
|
const merged = { ...prev, ...newState };
|
|
140
|
+
|
|
127
141
|
if (JSON.stringify(prev) !== JSON.stringify(merged)) {
|
|
128
142
|
console.log("🔄 State updated:", merged);
|
|
129
|
-
|
|
143
|
+
|
|
130
144
|
// Pause/resume countdown based on loading state
|
|
131
145
|
if (newState.isLoading !== undefined) {
|
|
132
146
|
if (newState.isLoading) {
|
|
@@ -137,14 +151,15 @@ const BiometricVerificationModal = React.memo(
|
|
|
137
151
|
resumeCountdown();
|
|
138
152
|
}
|
|
139
153
|
}
|
|
140
|
-
|
|
154
|
+
|
|
141
155
|
// Animate icon when step changes
|
|
142
156
|
if (newState.currentStep && newState.currentStep !== prev.currentStep) {
|
|
143
157
|
animateIcon();
|
|
144
158
|
}
|
|
145
|
-
|
|
159
|
+
|
|
146
160
|
return merged;
|
|
147
161
|
}
|
|
162
|
+
|
|
148
163
|
return prev;
|
|
149
164
|
});
|
|
150
165
|
} else {
|
|
@@ -152,9 +167,11 @@ const BiometricVerificationModal = React.memo(
|
|
|
152
167
|
}
|
|
153
168
|
}, [animateIcon, pauseCountdown, resumeCountdown]);
|
|
154
169
|
|
|
170
|
+
// Reset state helper
|
|
155
171
|
const resetState = useCallback(() => {
|
|
156
172
|
console.log("🔄 Resetting biometric modal state");
|
|
157
173
|
onclose(false);
|
|
174
|
+
|
|
158
175
|
setState({
|
|
159
176
|
isLoading: false,
|
|
160
177
|
loadingType: LOADING_TYPES.NONE,
|
|
@@ -162,6 +179,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
162
179
|
employeeData: null,
|
|
163
180
|
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
164
181
|
});
|
|
182
|
+
|
|
165
183
|
setModalVisible(false);
|
|
166
184
|
processedRef.current = false;
|
|
167
185
|
resetCountdown();
|
|
@@ -174,6 +192,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
174
192
|
}
|
|
175
193
|
}, [resetCountdown, clearNotification]);
|
|
176
194
|
|
|
195
|
+
// Error handler
|
|
177
196
|
const handleProcessError = useCallback(
|
|
178
197
|
(message, errorObj = null) => {
|
|
179
198
|
console.error("❌ Process Error:", message, errorObj || "");
|
|
@@ -189,6 +208,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
189
208
|
if (resetTimeoutRef.current) {
|
|
190
209
|
clearTimeout(resetTimeoutRef.current);
|
|
191
210
|
}
|
|
211
|
+
|
|
192
212
|
resetTimeoutRef.current = setTimeout(() => {
|
|
193
213
|
console.log("⏰ Error timeout completed - resetting state");
|
|
194
214
|
resetState();
|
|
@@ -197,25 +217,30 @@ const BiometricVerificationModal = React.memo(
|
|
|
197
217
|
[notifyMessage, resetState, updateState]
|
|
198
218
|
);
|
|
199
219
|
|
|
220
|
+
// Countdown finish handler
|
|
200
221
|
const handleCountdownFinish = useCallback(() => {
|
|
201
222
|
console.log("⏰ Countdown finished");
|
|
202
223
|
handleProcessError("Time is up! Please try again.");
|
|
224
|
+
|
|
203
225
|
if (navigation.canGoBack()) {
|
|
204
226
|
console.log("↩️ Navigating back due to timeout");
|
|
205
227
|
navigation.goBack();
|
|
206
228
|
}
|
|
207
229
|
}, [handleProcessError, navigation]);
|
|
208
230
|
|
|
231
|
+
// API URL validation
|
|
209
232
|
const validateApiUrl = useCallback(() => {
|
|
210
233
|
if (!apiurl || typeof apiurl !== "string") {
|
|
211
234
|
console.error("❌ Invalid API URL:", apiurl);
|
|
212
235
|
handleProcessError("Invalid API URL configuration.");
|
|
213
236
|
return false;
|
|
214
237
|
}
|
|
238
|
+
|
|
215
239
|
console.log("✅ API URL validated:", apiurl);
|
|
216
240
|
return true;
|
|
217
241
|
}, [apiurl, handleProcessError]);
|
|
218
242
|
|
|
243
|
+
// Face scan upload
|
|
219
244
|
const uploadFaceScan = useCallback(
|
|
220
245
|
async (selfie) => {
|
|
221
246
|
console.log("📸 Uploading face scan");
|
|
@@ -237,6 +262,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
237
262
|
|
|
238
263
|
InteractionManager.runAfterInteractions(async () => {
|
|
239
264
|
let base64;
|
|
265
|
+
|
|
240
266
|
try {
|
|
241
267
|
console.log("🖼️ Converting image to base64");
|
|
242
268
|
updateState({
|
|
@@ -279,6 +305,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
279
305
|
if (response?.httpstatus === 200) {
|
|
280
306
|
console.log("✅ Face recognition successful");
|
|
281
307
|
responseRef.current = response;
|
|
308
|
+
|
|
282
309
|
updateState({
|
|
283
310
|
employeeData: response.data?.data || null,
|
|
284
311
|
animationState: ANIMATION_STATES.SUCCESS,
|
|
@@ -294,9 +321,11 @@ const BiometricVerificationModal = React.memo(
|
|
|
294
321
|
} else {
|
|
295
322
|
console.log("✅ Verification complete - calling callback");
|
|
296
323
|
safeCallback(responseRef.current);
|
|
324
|
+
|
|
297
325
|
if (resetTimeoutRef.current) {
|
|
298
326
|
clearTimeout(resetTimeoutRef.current);
|
|
299
327
|
}
|
|
328
|
+
|
|
300
329
|
resetTimeoutRef.current = setTimeout(() => {
|
|
301
330
|
console.log("⏰ Success timeout completed - resetting");
|
|
302
331
|
resetState();
|
|
@@ -306,7 +335,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
306
335
|
console.warn("⚠️ Face recognition failed:", response?.data?.error);
|
|
307
336
|
handleProcessError(
|
|
308
337
|
response?.data?.error?.message ||
|
|
309
|
-
|
|
338
|
+
"Face not recognized. Please try again."
|
|
310
339
|
);
|
|
311
340
|
}
|
|
312
341
|
} catch (error) {
|
|
@@ -330,6 +359,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
330
359
|
]
|
|
331
360
|
);
|
|
332
361
|
|
|
362
|
+
// QR code processing
|
|
333
363
|
const handleQRScanned = useCallback(
|
|
334
364
|
async (qrCodeData) => {
|
|
335
365
|
console.log("🔍 Processing scanned QR code");
|
|
@@ -412,6 +442,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
412
442
|
console.log("✅ Location verified successfully");
|
|
413
443
|
safeCallback(responseRef.current);
|
|
414
444
|
notifyMessage("Location verified successfully!", "success");
|
|
445
|
+
|
|
415
446
|
updateState({
|
|
416
447
|
animationState: ANIMATION_STATES.SUCCESS,
|
|
417
448
|
isLoading: false,
|
|
@@ -421,6 +452,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
421
452
|
if (resetTimeoutRef.current) {
|
|
422
453
|
clearTimeout(resetTimeoutRef.current);
|
|
423
454
|
}
|
|
455
|
+
|
|
424
456
|
resetTimeoutRef.current = setTimeout(() => {
|
|
425
457
|
console.log("⏰ Location success timeout - resetting");
|
|
426
458
|
resetState();
|
|
@@ -464,6 +496,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
464
496
|
]
|
|
465
497
|
);
|
|
466
498
|
|
|
499
|
+
// Image capture handler
|
|
467
500
|
const handleImageCapture = useCallback(
|
|
468
501
|
async (capturedData) => {
|
|
469
502
|
console.log("📷 Image captured for step:", state.currentStep);
|
|
@@ -479,6 +512,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
479
512
|
[state.currentStep, uploadFaceScan, handleQRScanned]
|
|
480
513
|
);
|
|
481
514
|
|
|
515
|
+
// Start face scan
|
|
482
516
|
const handleStartFaceScan = useCallback(() => {
|
|
483
517
|
console.log("👤 Starting face scan");
|
|
484
518
|
updateState({
|
|
@@ -488,6 +522,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
488
522
|
setCameraType("front");
|
|
489
523
|
}, [updateState]);
|
|
490
524
|
|
|
525
|
+
// Start QR code scan
|
|
491
526
|
const startQRCodeScan = useCallback(() => {
|
|
492
527
|
console.log("📍 Starting QR code scan");
|
|
493
528
|
updateState({
|
|
@@ -497,6 +532,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
497
532
|
setCameraType("back");
|
|
498
533
|
}, [updateState]);
|
|
499
534
|
|
|
535
|
+
// Toggle camera
|
|
500
536
|
const toggleCamera = useCallback(() => {
|
|
501
537
|
console.log("🔄 Toggling camera");
|
|
502
538
|
setCameraType((prevType) => {
|
|
@@ -506,12 +542,14 @@ const BiometricVerificationModal = React.memo(
|
|
|
506
542
|
});
|
|
507
543
|
}, []);
|
|
508
544
|
|
|
545
|
+
// Start the verification process
|
|
509
546
|
const startProcess = useCallback(() => {
|
|
510
547
|
console.log("🚀 Starting verification process");
|
|
511
548
|
startCountdown(handleCountdownFinish);
|
|
512
549
|
handleStartFaceScan();
|
|
513
550
|
}, [handleCountdownFinish, handleStartFaceScan, startCountdown]);
|
|
514
551
|
|
|
552
|
+
// Open modal when data is received
|
|
515
553
|
useEffect(() => {
|
|
516
554
|
if (data && !modalVisible && !processedRef.current) {
|
|
517
555
|
console.log("📥 New data received, opening modal");
|
|
@@ -521,6 +559,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
521
559
|
}
|
|
522
560
|
}, [data, modalVisible, startProcess]);
|
|
523
561
|
|
|
562
|
+
// Loader source memoization
|
|
524
563
|
const loaderSource = useMemo(
|
|
525
564
|
() =>
|
|
526
565
|
state.isLoading &&
|
|
@@ -528,6 +567,7 @@ const BiometricVerificationModal = React.memo(
|
|
|
528
567
|
[state.isLoading, state.animationState, state.currentStep, apiurl]
|
|
529
568
|
);
|
|
530
569
|
|
|
570
|
+
// Determine if camera should be shown
|
|
531
571
|
const shouldShowCamera =
|
|
532
572
|
(state.currentStep === "Identity Verification" ||
|
|
533
573
|
state.currentStep === "Location Verification") &&
|
|
@@ -535,35 +575,17 @@ const BiometricVerificationModal = React.memo(
|
|
|
535
575
|
state.animationState !== ANIMATION_STATES.ERROR;
|
|
536
576
|
|
|
537
577
|
return (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
>
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
onPress={resetState}
|
|
550
|
-
accessibilityLabel="Close modal"
|
|
551
|
-
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
552
|
-
>
|
|
553
|
-
<Icon name="close" size={24} color={COLORS.light} />
|
|
554
|
-
</TouchableOpacity>
|
|
555
|
-
|
|
556
|
-
<View style={styles.headerContainer}>
|
|
557
|
-
|
|
558
|
-
<View style={styles.titleContainer}>
|
|
559
|
-
<Text style={styles.title}>Biometric Verification</Text>
|
|
560
|
-
<Text style={styles.subtitle}>{state.currentStep}</Text>
|
|
561
|
-
</View>
|
|
562
|
-
</View>
|
|
563
|
-
|
|
564
|
-
<StepIndicator currentStep={state.currentStep} qrscan={qrscan} />
|
|
565
|
-
|
|
566
|
-
{shouldShowCamera && !state.isLoading && (
|
|
578
|
+
<Modal
|
|
579
|
+
visible={modalVisible}
|
|
580
|
+
animationType="slide"
|
|
581
|
+
transparent
|
|
582
|
+
onRequestClose={resetState}
|
|
583
|
+
statusBarTranslucent
|
|
584
|
+
>
|
|
585
|
+
<View style={styles.modalContainer}>
|
|
586
|
+
{/* Camera component - full screen */}
|
|
587
|
+
{shouldShowCamera && !state.isLoading && (
|
|
588
|
+
<View style={styles.cameraContainer}>
|
|
567
589
|
<CaptureImageWithoutEdit
|
|
568
590
|
cameraType={cameraType}
|
|
569
591
|
onCapture={handleImageCapture}
|
|
@@ -571,30 +593,63 @@ const BiometricVerificationModal = React.memo(
|
|
|
571
593
|
showCodeScanner={state.currentStep === "Location Verification"}
|
|
572
594
|
isLoading={state.isLoading}
|
|
573
595
|
currentStep={state.currentStep}
|
|
596
|
+
frameProcessorFps={frameProcessorFps}
|
|
574
597
|
/>
|
|
598
|
+
</View>
|
|
599
|
+
)}
|
|
600
|
+
|
|
601
|
+
{/* UI elements positioned absolutely on top of camera */}
|
|
602
|
+
<TouchableOpacity
|
|
603
|
+
style={styles.closeButton}
|
|
604
|
+
onPress={resetState}
|
|
605
|
+
accessibilityLabel="Close modal"
|
|
606
|
+
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
607
|
+
>
|
|
608
|
+
<Icon name="close" size={24} color={COLORS.light} />
|
|
609
|
+
</TouchableOpacity>
|
|
610
|
+
|
|
611
|
+
<View style={styles.topContainer}>
|
|
612
|
+
{!shouldShowCamera && (
|
|
613
|
+
<View style={styles.headerContainer}>
|
|
614
|
+
<View style={styles.titleContainer}>
|
|
615
|
+
<Text style={styles.title}>Biometric Verification</Text>
|
|
616
|
+
<Text style={styles.subtitle}>{state.currentStep}</Text>
|
|
617
|
+
</View>
|
|
618
|
+
</View>
|
|
575
619
|
)}
|
|
620
|
+
</View>
|
|
621
|
+
|
|
622
|
+
<View style={styles.topContainerstep}>
|
|
623
|
+
<StepIndicator currentStep={state.currentStep} qrscan={qrscan} />
|
|
624
|
+
</View>
|
|
576
625
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
626
|
+
{state.employeeData && (
|
|
627
|
+
<View style={styles.cardContainer}>
|
|
628
|
+
<Card employeeData={state.employeeData} apiurl={apiurl} />
|
|
629
|
+
</View>
|
|
630
|
+
)}
|
|
580
631
|
|
|
632
|
+
<View style={styles.notificationContainer}>
|
|
581
633
|
<Notification
|
|
582
634
|
notification={notification}
|
|
583
635
|
fadeAnim={fadeAnim}
|
|
584
636
|
slideAnim={slideAnim}
|
|
585
637
|
/>
|
|
638
|
+
</View>
|
|
586
639
|
|
|
640
|
+
<View style={styles.timerContainer}>
|
|
587
641
|
<CountdownTimer
|
|
588
642
|
duration={COUNTDOWN_DURATION}
|
|
589
643
|
currentTime={countdown}
|
|
590
644
|
/>
|
|
591
|
-
<Loader
|
|
592
|
-
gifSource={loaderSource}
|
|
593
|
-
visible={state.isLoading}
|
|
594
|
-
/>
|
|
595
645
|
</View>
|
|
596
|
-
|
|
597
|
-
|
|
646
|
+
|
|
647
|
+
<Loader
|
|
648
|
+
gifSource={loaderSource}
|
|
649
|
+
visible={state.isLoading}
|
|
650
|
+
/>
|
|
651
|
+
</View>
|
|
652
|
+
</Modal>
|
|
598
653
|
);
|
|
599
654
|
}
|
|
600
655
|
);
|
|
@@ -603,18 +658,38 @@ const styles = StyleSheet.create({
|
|
|
603
658
|
modalContainer: {
|
|
604
659
|
flex: 1,
|
|
605
660
|
backgroundColor: COLORS.modalBackground || 'rgba(0, 0, 0, 0.85)',
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
661
|
+
},
|
|
662
|
+
cameraContainer: {
|
|
663
|
+
position: 'absolute',
|
|
664
|
+
top: 0,
|
|
665
|
+
left: 0,
|
|
666
|
+
right: 0,
|
|
667
|
+
bottom: 0,
|
|
668
|
+
zIndex: 0,
|
|
669
|
+
},
|
|
670
|
+
topContainer: {
|
|
671
|
+
position: 'absolute',
|
|
672
|
+
top: Platform.OS === 'ios' ? 50 : 30,
|
|
673
|
+
left: 0,
|
|
674
|
+
right: 0,
|
|
675
|
+
zIndex: 10,
|
|
676
|
+
paddingHorizontal: 20,
|
|
677
|
+
},
|
|
678
|
+
topContainerstep: {
|
|
679
|
+
position: 'absolute',
|
|
680
|
+
bottom: Platform.OS === 'ios' ? 50 : 30,
|
|
681
|
+
left: 0,
|
|
682
|
+
zIndex: 10,
|
|
609
683
|
},
|
|
610
684
|
headerContainer: {
|
|
611
685
|
flexDirection: 'row',
|
|
612
686
|
alignItems: 'center',
|
|
613
687
|
marginBottom: 15,
|
|
688
|
+
marginTop: 10,
|
|
614
689
|
},
|
|
615
690
|
titleContainer: {
|
|
616
691
|
flex: 1,
|
|
617
|
-
marginLeft:10
|
|
692
|
+
marginLeft: 10
|
|
618
693
|
},
|
|
619
694
|
title: {
|
|
620
695
|
fontSize: 26,
|
|
@@ -641,13 +716,36 @@ const styles = StyleSheet.create({
|
|
|
641
716
|
},
|
|
642
717
|
closeButton: {
|
|
643
718
|
position: 'absolute',
|
|
644
|
-
top: Platform.OS === 'ios' ?
|
|
719
|
+
top: Platform.OS === 'ios' ? 40 : 20,
|
|
645
720
|
right: 20,
|
|
646
|
-
zIndex:
|
|
721
|
+
zIndex: 20,
|
|
647
722
|
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
|
648
723
|
borderRadius: 20,
|
|
649
724
|
padding: 8,
|
|
650
725
|
},
|
|
726
|
+
cardContainer: {
|
|
727
|
+
position: 'absolute',
|
|
728
|
+
bottom: 100,
|
|
729
|
+
left: 0,
|
|
730
|
+
right: 0,
|
|
731
|
+
zIndex: 10,
|
|
732
|
+
paddingHorizontal: 20,
|
|
733
|
+
},
|
|
734
|
+
notificationContainer: {
|
|
735
|
+
position: 'absolute',
|
|
736
|
+
top: Platform.OS === 'ios' ? 120 : 100,
|
|
737
|
+
left: 0,
|
|
738
|
+
right: 0,
|
|
739
|
+
zIndex: 10,
|
|
740
|
+
paddingHorizontal: 20,
|
|
741
|
+
},
|
|
742
|
+
timerContainer: {
|
|
743
|
+
position: 'absolute',
|
|
744
|
+
bottom: Platform.OS === 'ios' ? 40 : 20,
|
|
745
|
+
left: 0,
|
|
746
|
+
right: 0,
|
|
747
|
+
zIndex: 10,
|
|
748
|
+
},
|
|
651
749
|
});
|
|
652
750
|
|
|
653
|
-
export default
|
|
751
|
+
export default BiometricModal;
|
|
@@ -20,7 +20,7 @@ const networkServiceCall = async (method, url, extraHeaders = {}, body = {}) =>
|
|
|
20
20
|
|
|
21
21
|
const response = await fetch(url, dataset);
|
|
22
22
|
|
|
23
|
-
console.log("🌐 Response Status:", response);
|
|
23
|
+
console.log("🌐 Response Status:", response.status);
|
|
24
24
|
|
|
25
25
|
const result = await response.json();
|
|
26
26
|
console.log("✅ API Success:", result);
|
|
@@ -32,4 +32,14 @@ const networkServiceCall = async (method, url, extraHeaders = {}, body = {}) =>
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// ✅ GET API Call helper
|
|
36
|
+
export const getApiCall = (url, extraHeaders = {}) => {
|
|
37
|
+
return networkServiceCall('GET', url, extraHeaders);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ✅ POST API Call helper
|
|
41
|
+
export const postApiCall = (url, body = {}, extraHeaders = {}) => {
|
|
42
|
+
return networkServiceCall('POST', url, extraHeaders, body);
|
|
43
|
+
};
|
|
44
|
+
|
|
35
45
|
export default networkServiceCall;
|