react-native-biometric-verifier 0.0.21 → 0.0.23
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
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
Text,
|
|
6
6
|
StyleSheet,
|
|
7
7
|
ActivityIndicator,
|
|
8
|
-
AppState,
|
|
9
|
-
BackHandler,
|
|
10
8
|
Animated,
|
|
11
9
|
} from 'react-native';
|
|
12
10
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
@@ -15,7 +13,6 @@ import {
|
|
|
15
13
|
getCameraDevice,
|
|
16
14
|
useCodeScanner,
|
|
17
15
|
useCameraFormat,
|
|
18
|
-
CameraRuntimeError,
|
|
19
16
|
} from 'react-native-vision-camera';
|
|
20
17
|
import { Global } from '../utils/Global';
|
|
21
18
|
import { useFaceDetectionFrameProcessor } from '../hooks/useFaceDetectionFrameProcessor';
|
|
@@ -27,7 +24,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
27
24
|
showCodeScanner = false,
|
|
28
25
|
isLoading = false,
|
|
29
26
|
frameProcessorFps = 1,
|
|
30
|
-
livenessLevel = 1,
|
|
27
|
+
livenessLevel = 1,
|
|
31
28
|
}) => {
|
|
32
29
|
const cameraRef = useRef(null);
|
|
33
30
|
const [cameraDevice, setCameraDevice] = useState(null);
|
|
@@ -35,6 +32,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
35
32
|
const [showCamera, setShowCamera] = useState(false);
|
|
36
33
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
37
34
|
const [currentCameraType, setCurrentCameraType] = useState(cameraType);
|
|
35
|
+
const [isInitializing, setIsInitializing] = useState(true);
|
|
38
36
|
|
|
39
37
|
const [faces, setFaces] = useState([]);
|
|
40
38
|
const [cameraError, setCameraError] = useState(null);
|
|
@@ -44,13 +42,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
44
42
|
const [faceCount, setFaceCount] = useState(0);
|
|
45
43
|
|
|
46
44
|
const captured = useRef(false);
|
|
47
|
-
const appState = useRef(AppState.currentState);
|
|
48
45
|
const isMounted = useRef(true);
|
|
49
46
|
|
|
50
|
-
// Animation values
|
|
51
47
|
const instructionAnim = useRef(new Animated.Value(1)).current;
|
|
52
48
|
|
|
53
|
-
// Reset capture state
|
|
54
49
|
const resetCaptureState = useCallback(() => {
|
|
55
50
|
captured.current = false;
|
|
56
51
|
setFaces([]);
|
|
@@ -60,7 +55,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
60
55
|
setFaceCount(0);
|
|
61
56
|
}, []);
|
|
62
57
|
|
|
63
|
-
// Code scanner
|
|
64
58
|
const codeScanner = useCodeScanner({
|
|
65
59
|
codeTypes: ['qr', 'ean-13'],
|
|
66
60
|
onCodeScanned: (codes) => {
|
|
@@ -76,46 +70,48 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
76
70
|
},
|
|
77
71
|
});
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
const onStableFaceDetected = useCallback(
|
|
74
|
+
async (faceRect) => {
|
|
75
|
+
if (!isMounted.current) return;
|
|
76
|
+
if (captured.current) return;
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
captured.current = true;
|
|
79
|
+
setFaces([faceRect]);
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
try {
|
|
82
|
+
if (!cameraRef.current) {
|
|
83
|
+
throw new Error('Camera ref not available');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const photo = await cameraRef.current.takePhoto({
|
|
87
|
+
flash: 'off',
|
|
88
|
+
qualityPrioritization: 'quality',
|
|
89
|
+
enableShutterSound: false,
|
|
90
|
+
skipMetadata: true,
|
|
91
|
+
});
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
enableShutterSound: false,
|
|
96
|
-
skipMetadata: true,
|
|
97
|
-
});
|
|
93
|
+
if (!photo || !photo.path) {
|
|
94
|
+
throw new Error('Failed to capture photo - no path returned');
|
|
95
|
+
}
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const photopath = `file://${photo.path}`;
|
|
98
|
+
const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
|
|
99
|
+
const photoData = {
|
|
100
|
+
uri: photopath,
|
|
101
|
+
filename: fileName,
|
|
102
|
+
filetype: 'image/jpeg',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
onCapture(photoData, faceRect);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error('Capture error:', e);
|
|
108
|
+
captured.current = false;
|
|
109
|
+
resetCaptureState();
|
|
110
|
+
setCameraError('Failed to capture image. Please try again.');
|
|
101
111
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const photoData = {
|
|
106
|
-
uri: photopath,
|
|
107
|
-
filename: fileName,
|
|
108
|
-
filetype: 'image/jpeg',
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
onCapture(photoData, faceRect);
|
|
112
|
-
} catch (e) {
|
|
113
|
-
console.error('Capture error:', e);
|
|
114
|
-
captured.current = false;
|
|
115
|
-
resetCaptureState();
|
|
116
|
-
setCameraError('Failed to capture image. Please try again.');
|
|
117
|
-
}
|
|
118
|
-
}, [onCapture, resetCaptureState]);
|
|
112
|
+
},
|
|
113
|
+
[onCapture, resetCaptureState]
|
|
114
|
+
);
|
|
119
115
|
|
|
120
116
|
const onFacesUpdate = useCallback((payload) => {
|
|
121
117
|
if (!isMounted.current) return;
|
|
@@ -125,7 +121,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
125
121
|
setProgress(progress);
|
|
126
122
|
|
|
127
123
|
if (count === 1) {
|
|
128
|
-
setFaces(prev => {
|
|
124
|
+
setFaces((prev) => {
|
|
129
125
|
if (prev.length === 1) return prev;
|
|
130
126
|
return [{ x: 0, y: 0, width: 0, height: 0 }];
|
|
131
127
|
});
|
|
@@ -137,20 +133,21 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
137
133
|
}
|
|
138
134
|
}, []);
|
|
139
135
|
|
|
140
|
-
const onLivenessUpdate = useCallback(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
136
|
+
const onLivenessUpdate = useCallback(
|
|
137
|
+
(step, extra) => {
|
|
138
|
+
setLivenessStep(step);
|
|
139
|
+
if (extra?.blinkCount !== undefined) setBlinkCount(extra.blinkCount);
|
|
140
|
+
|
|
141
|
+
instructionAnim.setValue(0);
|
|
142
|
+
Animated.timing(instructionAnim, {
|
|
143
|
+
toValue: 1,
|
|
144
|
+
duration: 300,
|
|
145
|
+
useNativeDriver: true,
|
|
146
|
+
}).start();
|
|
147
|
+
},
|
|
148
|
+
[instructionAnim]
|
|
149
|
+
);
|
|
152
150
|
|
|
153
|
-
// Use the face detection frame processor hook
|
|
154
151
|
const {
|
|
155
152
|
frameProcessor,
|
|
156
153
|
forceResetCaptureState,
|
|
@@ -164,10 +161,9 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
164
161
|
showCodeScanner,
|
|
165
162
|
isLoading,
|
|
166
163
|
isActive: showCamera && cameraInitialized,
|
|
167
|
-
livenessLevel,
|
|
164
|
+
livenessLevel,
|
|
168
165
|
});
|
|
169
166
|
|
|
170
|
-
// Sync captured state with the shared value
|
|
171
167
|
useEffect(() => {
|
|
172
168
|
if (capturedSV?.value && !captured.current) {
|
|
173
169
|
captured.current = true;
|
|
@@ -176,43 +172,63 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
176
172
|
}
|
|
177
173
|
}, [capturedSV?.value]);
|
|
178
174
|
|
|
179
|
-
// Get Camera Permission - from original code
|
|
180
175
|
const getPermission = useCallback(async () => {
|
|
181
176
|
try {
|
|
182
177
|
if (!isMounted.current) return;
|
|
183
178
|
|
|
179
|
+
setIsInitializing(true);
|
|
184
180
|
setCameraError(null);
|
|
181
|
+
setShowCamera(false);
|
|
182
|
+
|
|
185
183
|
const newCameraPermission = await Camera.requestCameraPermission();
|
|
186
184
|
setCameraPermission(newCameraPermission);
|
|
187
185
|
|
|
188
186
|
if (newCameraPermission === 'granted') {
|
|
189
187
|
const devices = await Camera.getAvailableCameraDevices();
|
|
190
|
-
|
|
188
|
+
|
|
189
|
+
if (!devices || devices.length === 0) {
|
|
190
|
+
throw new Error('No camera devices available');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const device = getCameraDevice(devices, currentCameraType);
|
|
194
|
+
|
|
195
|
+
if (!device) {
|
|
196
|
+
throw new Error(`No ${currentCameraType} camera available`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setCameraDevice(device);
|
|
191
200
|
setShowCamera(true);
|
|
201
|
+
console.log('Camera device set successfully');
|
|
192
202
|
} else {
|
|
193
203
|
setCameraError('Camera permission denied');
|
|
194
204
|
}
|
|
195
205
|
} catch (error) {
|
|
196
206
|
console.error('Camera permission error:', error);
|
|
197
|
-
setCameraError('Failed to get camera permission');
|
|
207
|
+
setCameraError(error.message || 'Failed to get camera permission');
|
|
208
|
+
setShowCamera(false);
|
|
209
|
+
} finally {
|
|
210
|
+
if (isMounted.current) {
|
|
211
|
+
setIsInitializing(false);
|
|
212
|
+
}
|
|
198
213
|
}
|
|
199
214
|
}, [currentCameraType]);
|
|
200
215
|
|
|
201
|
-
// Simplified camera initialization
|
|
202
216
|
const initializeCamera = useCallback(async () => {
|
|
203
|
-
|
|
204
|
-
if (!isMounted.current) return;
|
|
205
|
-
await getPermission();
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error('Camera initialization failed:', error);
|
|
208
|
-
setCameraError('Failed to initialize camera');
|
|
209
|
-
}
|
|
217
|
+
await getPermission();
|
|
210
218
|
}, [getPermission]);
|
|
211
219
|
|
|
212
|
-
// Effects: lifecycle, camera init, app state, cleanup
|
|
213
220
|
useEffect(() => {
|
|
214
221
|
isMounted.current = true;
|
|
215
|
-
|
|
222
|
+
|
|
223
|
+
const initOnMount = async () => {
|
|
224
|
+
try {
|
|
225
|
+
await initializeCamera();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Failed to initialize camera on mount:', error);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
initOnMount();
|
|
216
232
|
|
|
217
233
|
return () => {
|
|
218
234
|
isMounted.current = false;
|
|
@@ -221,105 +237,22 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
221
237
|
};
|
|
222
238
|
}, [initializeCamera, forceResetCaptureState]);
|
|
223
239
|
|
|
224
|
-
// Update frame processor active state when camera state changes
|
|
225
240
|
useEffect(() => {
|
|
226
241
|
updateIsActive(showCamera && cameraInitialized);
|
|
227
242
|
}, [showCamera, cameraInitialized, updateIsActive]);
|
|
228
243
|
|
|
229
|
-
// Handle camera type changes from parent
|
|
230
244
|
useEffect(() => {
|
|
231
245
|
if (cameraType !== currentCameraType) {
|
|
232
246
|
setCurrentCameraType(cameraType);
|
|
233
|
-
// Reinitialize camera with new type
|
|
234
247
|
initializeCamera();
|
|
235
248
|
}
|
|
236
249
|
}, [cameraType, currentCameraType, initializeCamera]);
|
|
237
250
|
|
|
238
|
-
// app state change
|
|
239
|
-
useEffect(() => {
|
|
240
|
-
const subscription = AppState.addEventListener('change', nextAppState => {
|
|
241
|
-
try {
|
|
242
|
-
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
243
|
-
if (cameraPermission === 'granted') {
|
|
244
|
-
setShowCamera(true);
|
|
245
|
-
if (captured.current) {
|
|
246
|
-
forceResetCaptureState();
|
|
247
|
-
resetCaptureState();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
} else if (nextAppState.match(/inactive|background/)) {
|
|
251
|
-
setShowCamera(false);
|
|
252
|
-
}
|
|
253
|
-
appState.current = nextAppState;
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.error('Error handling app state change:', error);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
return () => {
|
|
260
|
-
try {
|
|
261
|
-
subscription.remove();
|
|
262
|
-
} catch (error) {
|
|
263
|
-
console.error('Error removing app state listener:', error);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
}, [cameraPermission, forceResetCaptureState, resetCaptureState]);
|
|
267
|
-
|
|
268
|
-
// android back handler
|
|
269
|
-
useEffect(() => {
|
|
270
|
-
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
271
|
-
return true;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
return () => {
|
|
275
|
-
try {
|
|
276
|
-
backHandler.remove();
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.error('Error removing back handler:', error);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}, []);
|
|
282
|
-
|
|
283
|
-
// handle camera errors
|
|
284
|
-
const handleCameraError = useCallback((error) => {
|
|
285
|
-
console.error('Camera error:', error);
|
|
286
|
-
let errorMessage = 'Camera error occurred';
|
|
287
|
-
|
|
288
|
-
if (error instanceof CameraRuntimeError) {
|
|
289
|
-
switch (error.code) {
|
|
290
|
-
case 'session/configuration-failed':
|
|
291
|
-
errorMessage = 'Camera configuration failed. Please try again.';
|
|
292
|
-
break;
|
|
293
|
-
case 'device/not-available':
|
|
294
|
-
errorMessage = 'Camera not available';
|
|
295
|
-
break;
|
|
296
|
-
case 'permission/microphone-not-granted':
|
|
297
|
-
errorMessage = 'Camera permission required';
|
|
298
|
-
break;
|
|
299
|
-
default:
|
|
300
|
-
errorMessage = `Camera error: ${error.message}`;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
setCameraError(errorMessage);
|
|
305
|
-
setShowCamera(false);
|
|
306
|
-
forceResetCaptureState();
|
|
307
|
-
resetCaptureState();
|
|
308
|
-
}, [forceResetCaptureState, resetCaptureState]);
|
|
309
|
-
|
|
310
|
-
const handleCameraInitialized = useCallback(() => {
|
|
311
|
-
setCameraInitialized(true);
|
|
312
|
-
setCameraError(null);
|
|
313
|
-
updateIsActive(true);
|
|
314
|
-
}, [updateIsActive]);
|
|
315
|
-
|
|
316
|
-
// format selection
|
|
317
251
|
const format = useCameraFormat(cameraDevice, [
|
|
318
252
|
{ videoResolution: { width: 640, height: 640 } },
|
|
319
253
|
{ fps: 30 },
|
|
320
254
|
]);
|
|
321
255
|
|
|
322
|
-
// keep worklet's showCodeScanner flag in sync
|
|
323
256
|
useEffect(() => {
|
|
324
257
|
try {
|
|
325
258
|
updateShowCodeScanner(!!showCodeScanner);
|
|
@@ -330,24 +263,27 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
330
263
|
} catch (error) {
|
|
331
264
|
console.error('Error updating code scanner:', error);
|
|
332
265
|
}
|
|
333
|
-
}, [
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
forceResetCaptureState();
|
|
340
|
-
resetCaptureState();
|
|
341
|
-
setTimeout(() => {
|
|
342
|
-
if (isMounted.current) {
|
|
343
|
-
initializeCamera();
|
|
344
|
-
}
|
|
345
|
-
}, 500);
|
|
346
|
-
}, [initializeCamera, resetCaptureState, forceResetCaptureState]);
|
|
266
|
+
}, [
|
|
267
|
+
showCodeScanner,
|
|
268
|
+
updateShowCodeScanner,
|
|
269
|
+
forceResetCaptureState,
|
|
270
|
+
resetCaptureState,
|
|
271
|
+
]);
|
|
347
272
|
|
|
348
|
-
|
|
273
|
+
const handleRetry = useCallback(async () => {
|
|
274
|
+
try {
|
|
275
|
+
setCameraError(null);
|
|
276
|
+
setShowCamera(false);
|
|
277
|
+
setCameraInitialized(false);
|
|
278
|
+
forceResetCaptureState();
|
|
279
|
+
resetCaptureState();
|
|
280
|
+
await initializeCamera();
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error('Retry failed:', error);
|
|
283
|
+
setCameraError('Failed to retry camera initialization');
|
|
284
|
+
}
|
|
285
|
+
}, [initializeCamera, resetCaptureState, forceResetCaptureState]);
|
|
349
286
|
|
|
350
|
-
// Helper text for user guidance based on liveness level
|
|
351
287
|
const getInstruction = useCallback(() => {
|
|
352
288
|
if (faceCount > 1) {
|
|
353
289
|
return 'Multiple faces detected';
|
|
@@ -405,19 +341,24 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
405
341
|
}
|
|
406
342
|
|
|
407
343
|
return 'Align your face in frame';
|
|
408
|
-
}, [
|
|
409
|
-
|
|
344
|
+
}, [
|
|
345
|
+
livenessLevel,
|
|
346
|
+
livenessStep,
|
|
347
|
+
blinkCount,
|
|
348
|
+
faces.length,
|
|
349
|
+
progress,
|
|
350
|
+
faceCount,
|
|
351
|
+
]);
|
|
410
352
|
|
|
411
|
-
// Get step configuration based on liveness level
|
|
412
353
|
const getStepConfig = useCallback(() => {
|
|
413
354
|
switch (livenessLevel) {
|
|
414
|
-
case 0:
|
|
355
|
+
case 0:
|
|
415
356
|
return { totalSteps: 0, showSteps: false };
|
|
416
|
-
case 1:
|
|
357
|
+
case 1:
|
|
417
358
|
return { totalSteps: 1, showSteps: true };
|
|
418
|
-
case 2:
|
|
359
|
+
case 2:
|
|
419
360
|
return { totalSteps: 2, showSteps: true };
|
|
420
|
-
case 3:
|
|
361
|
+
case 3:
|
|
421
362
|
return { totalSteps: 3, showSteps: true };
|
|
422
363
|
default:
|
|
423
364
|
return { totalSteps: 0, showSteps: false };
|
|
@@ -426,7 +367,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
426
367
|
|
|
427
368
|
const stepConfig = getStepConfig();
|
|
428
369
|
|
|
429
|
-
// Get instruction text style based on content
|
|
430
370
|
const getInstructionStyle = useCallback(() => {
|
|
431
371
|
if (faceCount > 1) {
|
|
432
372
|
return [styles.instructionText, { color: Global.AppTheme.error }];
|
|
@@ -434,84 +374,38 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
434
374
|
return styles.instructionText;
|
|
435
375
|
}, [faceCount]);
|
|
436
376
|
|
|
437
|
-
// camera placeholder / UI
|
|
438
|
-
const renderCameraPlaceholder = () => (
|
|
439
|
-
<View style={styles.cameraContainer}>
|
|
440
|
-
{cameraError ? (
|
|
441
|
-
<View style={styles.errorContainer}>
|
|
442
|
-
<Icon name="error-outline" size={40} color={Global.AppTheme.error} />
|
|
443
|
-
<Text style={styles.errorText}>{cameraError}</Text>
|
|
444
|
-
<TouchableOpacity
|
|
445
|
-
style={styles.retryButton}
|
|
446
|
-
onPress={handleRetry}
|
|
447
|
-
accessibilityLabel="Retry camera initialization"
|
|
448
|
-
>
|
|
449
|
-
<Text style={styles.retryButtonText}>Retry</Text>
|
|
450
|
-
</TouchableOpacity>
|
|
451
|
-
</View>
|
|
452
|
-
) : cameraPermission === 'denied' || cameraPermission === 'restricted' ? (
|
|
453
|
-
<View style={styles.placeholderContainer}>
|
|
454
|
-
<Icon name="camera-off" size={40} color={Global.AppTheme.light} />
|
|
455
|
-
<Text style={styles.placeholderText}>
|
|
456
|
-
Camera permission required. Please enable in settings.
|
|
457
|
-
</Text>
|
|
458
|
-
<TouchableOpacity
|
|
459
|
-
style={styles.retryButton}
|
|
460
|
-
onPress={handleRetry}
|
|
461
|
-
accessibilityLabel="Request camera permission again"
|
|
462
|
-
>
|
|
463
|
-
<Text style={styles.retryButtonText}>Request Again</Text>
|
|
464
|
-
</TouchableOpacity>
|
|
465
|
-
</View>
|
|
466
|
-
) : cameraPermission === 'not-determined' ? (
|
|
467
|
-
<View style={styles.placeholderContainer}>
|
|
468
|
-
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
469
|
-
<Text style={styles.placeholderText}>
|
|
470
|
-
Requesting camera access...
|
|
471
|
-
</Text>
|
|
472
|
-
</View>
|
|
473
|
-
) : !cameraDevice ? (
|
|
474
|
-
<View style={styles.placeholderContainer}>
|
|
475
|
-
<Icon name="camera-alt" size={40} color={Global.AppTheme.light} />
|
|
476
|
-
<Text style={styles.placeholderText}>Camera not available</Text>
|
|
477
|
-
<TouchableOpacity
|
|
478
|
-
style={styles.retryButton}
|
|
479
|
-
onPress={handleRetry}
|
|
480
|
-
accessibilityLabel="Retry camera initialization"
|
|
481
|
-
>
|
|
482
|
-
<Text style={styles.retryButtonText}>Retry</Text>
|
|
483
|
-
</TouchableOpacity>
|
|
484
|
-
</View>
|
|
485
|
-
) : (
|
|
486
|
-
<View style={styles.placeholderContainer}>
|
|
487
|
-
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
488
|
-
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
489
|
-
</View>
|
|
490
|
-
)}
|
|
491
|
-
</View>
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
const shouldRenderCamera = showCamera &&
|
|
495
|
-
cameraPermission === 'granted' &&
|
|
496
|
-
cameraDevice &&
|
|
497
|
-
!cameraError;
|
|
498
|
-
|
|
499
377
|
return (
|
|
500
378
|
<View style={styles.container}>
|
|
501
379
|
<View style={styles.cameraContainer}>
|
|
502
|
-
{
|
|
380
|
+
{isInitializing && (
|
|
381
|
+
<View style={styles.loadingContainer}>
|
|
382
|
+
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
383
|
+
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
384
|
+
</View>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
{!isInitializing && showCamera && cameraDevice ? (
|
|
503
388
|
<Camera
|
|
504
389
|
ref={cameraRef}
|
|
505
390
|
style={styles.camera}
|
|
506
391
|
device={cameraDevice}
|
|
507
|
-
isActive={showCamera && !isLoading
|
|
392
|
+
isActive={showCamera && !isLoading}
|
|
508
393
|
photo={true}
|
|
509
394
|
format={format}
|
|
510
395
|
codeScanner={showCodeScanner ? codeScanner : undefined}
|
|
511
|
-
frameProcessor={
|
|
396
|
+
frameProcessor={
|
|
397
|
+
!showCodeScanner && cameraInitialized ? frameProcessor : undefined
|
|
398
|
+
}
|
|
512
399
|
frameProcessorFps={frameProcessorFps}
|
|
513
|
-
onInitialized={
|
|
514
|
-
|
|
400
|
+
onInitialized={() => {
|
|
401
|
+
console.log('Camera initialized successfully');
|
|
402
|
+
setCameraInitialized(true);
|
|
403
|
+
}}
|
|
404
|
+
onError={(error) => {
|
|
405
|
+
console.log('Camera error:', error);
|
|
406
|
+
setCameraError('Camera error occurred');
|
|
407
|
+
setCameraInitialized(false);
|
|
408
|
+
}}
|
|
515
409
|
enableZoomGesture={false}
|
|
516
410
|
exposure={0}
|
|
517
411
|
pixelFormat="yuv"
|
|
@@ -519,13 +413,23 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
519
413
|
orientation="portrait"
|
|
520
414
|
/>
|
|
521
415
|
) : (
|
|
522
|
-
|
|
416
|
+
<View style={styles.placeholderContainer}>
|
|
417
|
+
<Icon name="camera-alt" size={40} color={Global.AppTheme.light} />
|
|
418
|
+
<Text style={styles.placeholderText}>
|
|
419
|
+
{cameraError || 'Camera not available'}
|
|
420
|
+
</Text>
|
|
421
|
+
<TouchableOpacity
|
|
422
|
+
style={styles.retryButton}
|
|
423
|
+
onPress={handleRetry}
|
|
424
|
+
accessibilityLabel="Retry camera initialization"
|
|
425
|
+
>
|
|
426
|
+
<Text style={styles.retryButtonText}>Retry</Text>
|
|
427
|
+
</TouchableOpacity>
|
|
428
|
+
</View>
|
|
523
429
|
)}
|
|
524
430
|
|
|
525
|
-
{
|
|
526
|
-
{!showCodeScanner && shouldRenderCamera && livenessLevel > 0 && (
|
|
431
|
+
{!showCodeScanner && showCamera && cameraDevice && livenessLevel > 0 && (
|
|
527
432
|
<View style={styles.livenessContainer}>
|
|
528
|
-
{/* Instructions with animation */}
|
|
529
433
|
<Animated.View
|
|
530
434
|
style={[
|
|
531
435
|
styles.instructionContainer,
|
|
@@ -545,45 +449,54 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
545
449
|
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
546
450
|
</Animated.View>
|
|
547
451
|
|
|
548
|
-
{
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
452
|
+
{(livenessLevel === 1 || livenessLevel === 3) &&
|
|
453
|
+
livenessStep === (livenessLevel === 1 ? 1 : 3) && (
|
|
454
|
+
<View style={styles.blinkProgressContainer}>
|
|
455
|
+
{[1, 2, 3].map((i) => (
|
|
456
|
+
<View
|
|
457
|
+
key={i}
|
|
458
|
+
style={[
|
|
459
|
+
styles.blinkDot,
|
|
460
|
+
blinkCount >= i && styles.blinkDotActive,
|
|
461
|
+
]}
|
|
462
|
+
/>
|
|
463
|
+
))}
|
|
464
|
+
</View>
|
|
465
|
+
)}
|
|
562
466
|
|
|
563
|
-
{/* Step Indicators - only show for liveness levels 1, 2, 3 */}
|
|
564
467
|
{stepConfig.showSteps && faceCount <= 1 && (
|
|
565
468
|
<>
|
|
566
469
|
<View style={styles.stepsContainer}>
|
|
567
|
-
{Array.from({ length: stepConfig.totalSteps + 1 }).map(
|
|
568
|
-
|
|
569
|
-
<
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
470
|
+
{Array.from({ length: stepConfig.totalSteps + 1 }).map(
|
|
471
|
+
(_, step) => (
|
|
472
|
+
<React.Fragment key={step}>
|
|
473
|
+
<View
|
|
474
|
+
style={[
|
|
475
|
+
styles.stepIndicator,
|
|
476
|
+
livenessStep > step
|
|
477
|
+
? styles.stepCompleted
|
|
478
|
+
: livenessStep === step
|
|
479
|
+
? styles.stepCurrent
|
|
480
|
+
: styles.stepPending,
|
|
481
|
+
]}
|
|
482
|
+
>
|
|
483
|
+
<Text style={styles.stepText}>{step + 1}</Text>
|
|
484
|
+
</View>
|
|
485
|
+
{step < stepConfig.totalSteps && (
|
|
486
|
+
<View
|
|
487
|
+
style={[
|
|
488
|
+
styles.stepConnector,
|
|
489
|
+
livenessStep > step
|
|
490
|
+
? styles.connectorCompleted
|
|
491
|
+
: {},
|
|
492
|
+
]}
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
</React.Fragment>
|
|
496
|
+
)
|
|
497
|
+
)}
|
|
584
498
|
</View>
|
|
585
499
|
|
|
586
|
-
{/* Step Labels */}
|
|
587
500
|
<View style={styles.stepLabelsContainer}>
|
|
588
501
|
{livenessLevel === 1 && (
|
|
589
502
|
<>
|
|
@@ -612,8 +525,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
612
525
|
</View>
|
|
613
526
|
)}
|
|
614
527
|
|
|
615
|
-
{
|
|
616
|
-
{!showCodeScanner && shouldRenderCamera && livenessLevel === 0 && (
|
|
528
|
+
{!showCodeScanner && showCamera && cameraDevice && livenessLevel === 0 && (
|
|
617
529
|
<View style={styles.livenessContainer}>
|
|
618
530
|
<Animated.View
|
|
619
531
|
style={[
|
|
@@ -626,14 +538,13 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
626
538
|
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
627
539
|
</Animated.View>
|
|
628
540
|
|
|
629
|
-
{/* Progress bar for stability - only show when single face detected */}
|
|
630
541
|
{faceCount === 1 && (
|
|
631
542
|
<View style={styles.stabilityContainer}>
|
|
632
543
|
<View style={styles.stabilityBar}>
|
|
633
544
|
<View
|
|
634
545
|
style={[
|
|
635
546
|
styles.stabilityProgress,
|
|
636
|
-
{ width: `${progress}%` }
|
|
547
|
+
{ width: `${progress}%` },
|
|
637
548
|
]}
|
|
638
549
|
/>
|
|
639
550
|
</View>
|
|
@@ -666,41 +577,23 @@ const styles = StyleSheet.create({
|
|
|
666
577
|
flex: 1,
|
|
667
578
|
width: '100%',
|
|
668
579
|
},
|
|
669
|
-
|
|
670
|
-
position: 'absolute',
|
|
671
|
-
bottom: 20,
|
|
672
|
-
right: 20,
|
|
673
|
-
alignItems: 'center',
|
|
674
|
-
},
|
|
675
|
-
cameraSwitchButton: {
|
|
676
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
677
|
-
padding: 12,
|
|
678
|
-
borderRadius: 30,
|
|
679
|
-
alignItems: 'center',
|
|
680
|
-
justifyContent: 'center',
|
|
681
|
-
},
|
|
682
|
-
controlText: {
|
|
683
|
-
color: 'white',
|
|
684
|
-
fontSize: 10,
|
|
685
|
-
marginTop: 4,
|
|
686
|
-
},
|
|
687
|
-
placeholderContainer: {
|
|
580
|
+
loadingContainer: {
|
|
688
581
|
flex: 1,
|
|
689
582
|
justifyContent: 'center',
|
|
690
583
|
alignItems: 'center',
|
|
691
584
|
padding: 20,
|
|
692
585
|
},
|
|
693
|
-
|
|
586
|
+
placeholderContainer: {
|
|
694
587
|
flex: 1,
|
|
695
588
|
justifyContent: 'center',
|
|
696
589
|
alignItems: 'center',
|
|
697
590
|
padding: 20,
|
|
698
591
|
},
|
|
699
|
-
|
|
700
|
-
color: Global.AppTheme.
|
|
592
|
+
placeholderText: {
|
|
593
|
+
color: Global.AppTheme.light,
|
|
701
594
|
fontSize: 16,
|
|
702
595
|
textAlign: 'center',
|
|
703
|
-
|
|
596
|
+
marginTop: 16,
|
|
704
597
|
},
|
|
705
598
|
retryButton: {
|
|
706
599
|
backgroundColor: Global.AppTheme.primary,
|
|
@@ -713,13 +606,6 @@ const styles = StyleSheet.create({
|
|
|
713
606
|
color: Global.AppTheme.light,
|
|
714
607
|
fontWeight: 'bold',
|
|
715
608
|
},
|
|
716
|
-
placeholderText: {
|
|
717
|
-
color: Global.AppTheme.light,
|
|
718
|
-
fontSize: 16,
|
|
719
|
-
textAlign: 'center',
|
|
720
|
-
marginTop: 16,
|
|
721
|
-
},
|
|
722
|
-
// Liveness detection styles
|
|
723
609
|
livenessContainer: {
|
|
724
610
|
position: 'absolute',
|
|
725
611
|
top: 40,
|
|
@@ -808,7 +694,6 @@ const styles = StyleSheet.create({
|
|
|
808
694
|
textAlign: 'center',
|
|
809
695
|
flex: 1,
|
|
810
696
|
},
|
|
811
|
-
// Stability bar for level 0
|
|
812
697
|
stabilityContainer: {
|
|
813
698
|
alignItems: 'center',
|
|
814
699
|
width: '100%',
|
package/src/components/Card.js
CHANGED
|
@@ -4,7 +4,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import { Global } from '../utils/Global';
|
|
6
6
|
|
|
7
|
-
export const Card = ({ employeeData, apiurl,
|
|
7
|
+
export const Card = ({ employeeData, apiurl, fileurl = 'file/filedownload/photo/' }) => {
|
|
8
8
|
const [imageError, setImageError] = useState(false);
|
|
9
9
|
|
|
10
10
|
if (!employeeData || typeof employeeData !== 'object') {
|
|
@@ -18,7 +18,7 @@ export const Card = ({ employeeData, apiurl, imageurl = 'file/filedownload/photo
|
|
|
18
18
|
const employeeId = faceid || 'N/A';
|
|
19
19
|
|
|
20
20
|
const imageSource = !imageError && img
|
|
21
|
-
? { uri: `${apiurl}${
|
|
21
|
+
? { uri: `${apiurl}${fileurl}${img}` }
|
|
22
22
|
: null;
|
|
23
23
|
|
|
24
24
|
return (
|
|
@@ -65,7 +65,7 @@ Card.propTypes = {
|
|
|
65
65
|
img: PropTypes.string,
|
|
66
66
|
}),
|
|
67
67
|
apiurl: PropTypes.string,
|
|
68
|
-
|
|
68
|
+
fileurl: PropTypes.string,
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
const styles = StyleSheet.create({
|
package/src/components/Loader.js
CHANGED
|
@@ -16,7 +16,7 @@ export default function Loader({
|
|
|
16
16
|
overlayColor = 'rgba(0,0,0,0.4)',
|
|
17
17
|
loaderColor = 'lightblue',
|
|
18
18
|
size = 50,
|
|
19
|
-
gifSource
|
|
19
|
+
gifSource,
|
|
20
20
|
message = '',
|
|
21
21
|
messageStyle = {},
|
|
22
22
|
animationType = 'fade',
|
|
@@ -6,7 +6,7 @@ import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
|
6
6
|
// Tuned constants for liveness detection / stability
|
|
7
7
|
const FACE_STABILITY_THRESHOLD = 3;
|
|
8
8
|
const FACE_MOVEMENT_THRESHOLD = 15;
|
|
9
|
-
const FRAME_PROCESSOR_MIN_INTERVAL_MS =
|
|
9
|
+
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 800; // increased to reduce load
|
|
10
10
|
const MIN_FACE_SIZE = 0.2;
|
|
11
11
|
|
|
12
12
|
// Liveness detection constants
|
package/src/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
|
40
40
|
import StepIndicator from "./components/StepIndicator";
|
|
41
41
|
|
|
42
42
|
const BiometricModal = React.memo(
|
|
43
|
-
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, imageurl }) => {
|
|
43
|
+
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, fileurl, imageurl }) => {
|
|
44
44
|
const navigation = useNavigation();
|
|
45
45
|
|
|
46
46
|
// Custom hooks
|
|
@@ -547,8 +547,8 @@ const BiometricModal = React.memo(
|
|
|
547
547
|
const loaderSource = useMemo(
|
|
548
548
|
() =>
|
|
549
549
|
state.isLoading &&
|
|
550
|
-
getLoaderGif(state.animationState, state.currentStep, apiurl),
|
|
551
|
-
[state.isLoading, state.animationState, state.currentStep, apiurl]
|
|
550
|
+
getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl),
|
|
551
|
+
[state.isLoading, state.animationState, state.currentStep, apiurl, imageurl]
|
|
552
552
|
);
|
|
553
553
|
|
|
554
554
|
// Determine if camera should be shown
|
|
@@ -608,7 +608,7 @@ const BiometricModal = React.memo(
|
|
|
608
608
|
|
|
609
609
|
{state.employeeData && (
|
|
610
610
|
<View style={styles.cardContainer}>
|
|
611
|
-
<Card employeeData={state.employeeData} apiurl={apiurl}
|
|
611
|
+
<Card employeeData={state.employeeData} apiurl={apiurl} fileurl={fileurl} />
|
|
612
612
|
</View>
|
|
613
613
|
)}
|
|
614
614
|
|
|
@@ -6,11 +6,11 @@ import { Global } from "./Global";
|
|
|
6
6
|
* @param {string} currentStep - Current step of verification
|
|
7
7
|
* @returns {any} - Gif image source or null
|
|
8
8
|
*/
|
|
9
|
-
export const getLoaderGif = (animationState, currentStep,APIURL) => {
|
|
9
|
+
export const getLoaderGif = (animationState, currentStep,APIURL,imageurl ='file/getCommonFile/image/') => {
|
|
10
10
|
const FaceGifUrl =
|
|
11
|
-
`${APIURL}
|
|
11
|
+
`${APIURL}${imageurl}Face.gif`;
|
|
12
12
|
const LocationGifUrl =
|
|
13
|
-
`${APIURL}
|
|
13
|
+
`${APIURL}${imageurl}Location.gif`;
|
|
14
14
|
|
|
15
15
|
if (
|
|
16
16
|
animationState === Global.AnimationStates.faceScan ||
|