react-native-biometric-verifier 0.0.19 → 0.0.20
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 +357 -259
- package/src/hooks/useFaceDetectionFrameProcessor.js +378 -261
- package/src/index.js +2 -13
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
Text,
|
|
6
6
|
StyleSheet,
|
|
7
7
|
ActivityIndicator,
|
|
8
|
-
Platform,
|
|
9
8
|
AppState,
|
|
10
9
|
BackHandler,
|
|
11
10
|
Animated,
|
|
@@ -25,28 +24,28 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
25
24
|
({
|
|
26
25
|
cameraType = 'front',
|
|
27
26
|
onCapture,
|
|
28
|
-
onToggleCamera,
|
|
29
27
|
showCodeScanner = false,
|
|
30
28
|
isLoading = false,
|
|
31
|
-
currentStep = '',
|
|
32
29
|
frameProcessorFps = 1,
|
|
30
|
+
livenessLevel = 1, // 0=no liveness, 1=blink only, 2=face turn only, 3=both
|
|
33
31
|
}) => {
|
|
34
32
|
const cameraRef = useRef(null);
|
|
35
33
|
const [cameraDevice, setCameraDevice] = useState(null);
|
|
36
34
|
const [cameraPermission, setCameraPermission] = useState('not-determined');
|
|
37
35
|
const [showCamera, setShowCamera] = useState(false);
|
|
38
36
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
37
|
+
const [currentCameraType, setCurrentCameraType] = useState(cameraType);
|
|
39
38
|
|
|
40
39
|
const [faces, setFaces] = useState([]);
|
|
41
|
-
const [singleFaceDetected, setSingleFaceDetected] = useState(false);
|
|
42
40
|
const [cameraError, setCameraError] = useState(null);
|
|
43
41
|
const [livenessStep, setLivenessStep] = useState(0);
|
|
42
|
+
const [blinkCount, setBlinkCount] = useState(0);
|
|
43
|
+
const [progress, setProgress] = useState(0);
|
|
44
|
+
const [faceCount, setFaceCount] = useState(0);
|
|
44
45
|
|
|
45
46
|
const captured = useRef(false);
|
|
46
47
|
const appState = useRef(AppState.currentState);
|
|
47
48
|
const isMounted = useRef(true);
|
|
48
|
-
const initializationAttempts = useRef(0);
|
|
49
|
-
const maxInitializationAttempts = 3;
|
|
50
49
|
|
|
51
50
|
// Animation values
|
|
52
51
|
const instructionAnim = useRef(new Animated.Value(1)).current;
|
|
@@ -54,9 +53,11 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
54
53
|
// Reset capture state
|
|
55
54
|
const resetCaptureState = useCallback(() => {
|
|
56
55
|
captured.current = false;
|
|
57
|
-
setSingleFaceDetected(false);
|
|
58
56
|
setFaces([]);
|
|
59
57
|
setLivenessStep(0);
|
|
58
|
+
setBlinkCount(0);
|
|
59
|
+
setProgress(0);
|
|
60
|
+
setFaceCount(0);
|
|
60
61
|
}, []);
|
|
61
62
|
|
|
62
63
|
// Code scanner
|
|
@@ -76,61 +77,59 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
// Callbacks for face detection events
|
|
79
|
-
const onStableFaceDetected = useCallback((faceRect) => {
|
|
80
|
+
const onStableFaceDetected = useCallback(async (faceRect) => {
|
|
80
81
|
if (!isMounted.current) return;
|
|
81
82
|
if (captured.current) return;
|
|
82
83
|
|
|
83
84
|
captured.current = true;
|
|
84
|
-
setSingleFaceDetected(true);
|
|
85
85
|
setFaces([faceRect]);
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const photo = await cameraRef.current.takePhoto({
|
|
94
|
-
flash: 'off',
|
|
95
|
-
qualityPrioritization: 'balanced',
|
|
96
|
-
enableShutterSound: false,
|
|
97
|
-
skipMetadata: true,
|
|
98
|
-
});
|
|
87
|
+
try {
|
|
88
|
+
if (!cameraRef.current) {
|
|
89
|
+
throw new Error('Camera ref not available');
|
|
90
|
+
}
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
const photo = await cameraRef.current.takePhoto({
|
|
93
|
+
flash: 'off',
|
|
94
|
+
qualityPrioritization: 'quality',
|
|
95
|
+
enableShutterSound: false,
|
|
96
|
+
skipMetadata: true,
|
|
97
|
+
});
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const photoData = {
|
|
107
|
-
uri: photopath,
|
|
108
|
-
filename: fileName,
|
|
109
|
-
filetype: 'image/jpeg',
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
onCapture(photoData);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error('Capture error:', e);
|
|
115
|
-
captured.current = false;
|
|
116
|
-
resetCaptureState();
|
|
117
|
-
setCameraError('Failed to capture image. Please try again.');
|
|
99
|
+
if (!photo || !photo.path) {
|
|
100
|
+
throw new Error('Failed to capture photo - no path returned');
|
|
118
101
|
}
|
|
119
|
-
|
|
102
|
+
|
|
103
|
+
const photopath = `file://${photo.path}`;
|
|
104
|
+
const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
|
|
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
|
+
}
|
|
120
118
|
}, [onCapture, resetCaptureState]);
|
|
121
119
|
|
|
122
120
|
const onFacesUpdate = useCallback((payload) => {
|
|
123
121
|
if (!isMounted.current) return;
|
|
124
122
|
try {
|
|
125
|
-
const { count } = payload;
|
|
123
|
+
const { count, progress } = payload;
|
|
124
|
+
setFaceCount(count);
|
|
125
|
+
setProgress(progress);
|
|
126
|
+
|
|
126
127
|
if (count === 1) {
|
|
127
|
-
setSingleFaceDetected(true);
|
|
128
128
|
setFaces(prev => {
|
|
129
129
|
if (prev.length === 1) return prev;
|
|
130
130
|
return [{ x: 0, y: 0, width: 0, height: 0 }];
|
|
131
131
|
});
|
|
132
132
|
} else {
|
|
133
|
-
setSingleFaceDetected(false);
|
|
134
133
|
setFaces([]);
|
|
135
134
|
}
|
|
136
135
|
} catch (error) {
|
|
@@ -138,9 +137,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
138
137
|
}
|
|
139
138
|
}, []);
|
|
140
139
|
|
|
141
|
-
const onLivenessUpdate = useCallback((step) => {
|
|
140
|
+
const onLivenessUpdate = useCallback((step, extra) => {
|
|
142
141
|
setLivenessStep(step);
|
|
143
|
-
|
|
142
|
+
if (extra?.blinkCount !== undefined) setBlinkCount(extra.blinkCount);
|
|
143
|
+
|
|
144
144
|
// Animate instruction change
|
|
145
145
|
instructionAnim.setValue(0);
|
|
146
146
|
Animated.timing(instructionAnim, {
|
|
@@ -153,7 +153,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
153
153
|
// Use the face detection frame processor hook
|
|
154
154
|
const {
|
|
155
155
|
frameProcessor,
|
|
156
|
-
resetCaptureState: resetFrameProcessor,
|
|
157
156
|
forceResetCaptureState,
|
|
158
157
|
updateShowCodeScanner,
|
|
159
158
|
updateIsActive,
|
|
@@ -164,69 +163,51 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
164
163
|
onLivenessUpdate,
|
|
165
164
|
showCodeScanner,
|
|
166
165
|
isLoading,
|
|
167
|
-
isActive: showCamera && cameraInitialized,
|
|
166
|
+
isActive: showCamera && cameraInitialized,
|
|
167
|
+
livenessLevel, // Pass liveness level to hook
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
// Sync captured state with the shared value
|
|
171
171
|
useEffect(() => {
|
|
172
172
|
if (capturedSV?.value && !captured.current) {
|
|
173
173
|
captured.current = true;
|
|
174
|
-
setSingleFaceDetected(true);
|
|
175
174
|
} else if (!capturedSV?.value && captured.current) {
|
|
176
175
|
captured.current = false;
|
|
177
|
-
setSingleFaceDetected(false);
|
|
178
176
|
}
|
|
179
177
|
}, [capturedSV?.value]);
|
|
180
178
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
179
|
+
// Get Camera Permission - from original code
|
|
180
|
+
const getPermission = useCallback(async () => {
|
|
183
181
|
try {
|
|
184
182
|
if (!isMounted.current) return;
|
|
185
|
-
if (initializationAttempts.current >= maxInitializationAttempts) {
|
|
186
|
-
setCameraError('Failed to initialize camera after multiple attempts');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
183
|
|
|
190
|
-
initializationAttempts.current += 1;
|
|
191
184
|
setCameraError(null);
|
|
185
|
+
const newCameraPermission = await Camera.requestCameraPermission();
|
|
186
|
+
setCameraPermission(newCameraPermission);
|
|
192
187
|
|
|
193
|
-
|
|
194
|
-
setCameraPermission(permission);
|
|
195
|
-
|
|
196
|
-
if (permission === 'granted') {
|
|
188
|
+
if (newCameraPermission === 'granted') {
|
|
197
189
|
const devices = await Camera.getAvailableCameraDevices();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw new Error('No camera devices found');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const device = getCameraDevice(devices, cameraType);
|
|
204
|
-
|
|
205
|
-
if (device) {
|
|
206
|
-
setCameraDevice(device);
|
|
207
|
-
setTimeout(() => setShowCamera(true), 100);
|
|
208
|
-
initializationAttempts.current = 0; // Reset attempts on success
|
|
209
|
-
} else {
|
|
210
|
-
throw new Error(`No camera device found for type: ${cameraType}`);
|
|
211
|
-
}
|
|
190
|
+
setCameraDevice(getCameraDevice(devices, currentCameraType));
|
|
191
|
+
setShowCamera(true);
|
|
212
192
|
} else {
|
|
213
|
-
|
|
193
|
+
setCameraError('Camera permission denied');
|
|
214
194
|
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Camera permission error:', error);
|
|
197
|
+
setCameraError('Failed to get camera permission');
|
|
198
|
+
}
|
|
199
|
+
}, [currentCameraType]);
|
|
200
|
+
|
|
201
|
+
// Simplified camera initialization
|
|
202
|
+
const initializeCamera = useCallback(async () => {
|
|
203
|
+
try {
|
|
204
|
+
if (!isMounted.current) return;
|
|
205
|
+
await getPermission();
|
|
215
206
|
} catch (error) {
|
|
216
207
|
console.error('Camera initialization failed:', error);
|
|
217
|
-
|
|
218
|
-
let errorMessage = 'Failed to initialize camera';
|
|
219
|
-
|
|
220
|
-
if (error.message.includes('permission')) {
|
|
221
|
-
errorMessage = 'Camera permission denied';
|
|
222
|
-
} else if (error.message.includes('No camera devices')) {
|
|
223
|
-
errorMessage = 'No camera available on this device';
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
setCameraError(errorMessage);
|
|
227
|
-
}
|
|
208
|
+
setCameraError('Failed to initialize camera');
|
|
228
209
|
}
|
|
229
|
-
}, [
|
|
210
|
+
}, [getPermission]);
|
|
230
211
|
|
|
231
212
|
// Effects: lifecycle, camera init, app state, cleanup
|
|
232
213
|
useEffect(() => {
|
|
@@ -236,7 +217,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
236
217
|
return () => {
|
|
237
218
|
isMounted.current = false;
|
|
238
219
|
setShowCamera(false);
|
|
239
|
-
// Reset frame processor state on unmount
|
|
240
220
|
forceResetCaptureState();
|
|
241
221
|
};
|
|
242
222
|
}, [initializeCamera, forceResetCaptureState]);
|
|
@@ -246,6 +226,15 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
246
226
|
updateIsActive(showCamera && cameraInitialized);
|
|
247
227
|
}, [showCamera, cameraInitialized, updateIsActive]);
|
|
248
228
|
|
|
229
|
+
// Handle camera type changes from parent
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (cameraType !== currentCameraType) {
|
|
232
|
+
setCurrentCameraType(cameraType);
|
|
233
|
+
// Reinitialize camera with new type
|
|
234
|
+
initializeCamera();
|
|
235
|
+
}
|
|
236
|
+
}, [cameraType, currentCameraType, initializeCamera]);
|
|
237
|
+
|
|
249
238
|
// app state change
|
|
250
239
|
useEffect(() => {
|
|
251
240
|
const subscription = AppState.addEventListener('change', nextAppState => {
|
|
@@ -253,7 +242,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
253
242
|
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
254
243
|
if (cameraPermission === 'granted') {
|
|
255
244
|
setShowCamera(true);
|
|
256
|
-
// Reset capture state when app comes back to foreground
|
|
257
245
|
if (captured.current) {
|
|
258
246
|
forceResetCaptureState();
|
|
259
247
|
resetCaptureState();
|
|
@@ -280,10 +268,9 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
280
268
|
// android back handler
|
|
281
269
|
useEffect(() => {
|
|
282
270
|
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
283
|
-
// Don't allow back press in camera screen
|
|
284
271
|
return true;
|
|
285
272
|
});
|
|
286
|
-
|
|
273
|
+
|
|
287
274
|
return () => {
|
|
288
275
|
try {
|
|
289
276
|
backHandler.remove();
|
|
@@ -316,7 +303,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
316
303
|
|
|
317
304
|
setCameraError(errorMessage);
|
|
318
305
|
setShowCamera(false);
|
|
319
|
-
// Reset capture state on error
|
|
320
306
|
forceResetCaptureState();
|
|
321
307
|
resetCaptureState();
|
|
322
308
|
}, [forceResetCaptureState, resetCaptureState]);
|
|
@@ -324,7 +310,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
324
310
|
const handleCameraInitialized = useCallback(() => {
|
|
325
311
|
setCameraInitialized(true);
|
|
326
312
|
setCameraError(null);
|
|
327
|
-
// Ensure frame processor is active after initialization
|
|
328
313
|
updateIsActive(true);
|
|
329
314
|
}, [updateIsActive]);
|
|
330
315
|
|
|
@@ -338,7 +323,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
338
323
|
useEffect(() => {
|
|
339
324
|
try {
|
|
340
325
|
updateShowCodeScanner(!!showCodeScanner);
|
|
341
|
-
// Reset capture state when switching modes
|
|
342
326
|
if (showCodeScanner && captured.current) {
|
|
343
327
|
forceResetCaptureState();
|
|
344
328
|
resetCaptureState();
|
|
@@ -352,7 +336,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
352
336
|
const handleRetry = useCallback(() => {
|
|
353
337
|
setCameraError(null);
|
|
354
338
|
setShowCamera(false);
|
|
355
|
-
// Reset both local and frame processor state
|
|
356
339
|
forceResetCaptureState();
|
|
357
340
|
resetCaptureState();
|
|
358
341
|
setTimeout(() => {
|
|
@@ -362,27 +345,94 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
362
345
|
}, 500);
|
|
363
346
|
}, [initializeCamera, resetCaptureState, forceResetCaptureState]);
|
|
364
347
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
// Helper text for user guidance based on liveness level
|
|
351
|
+
const getInstruction = useCallback(() => {
|
|
352
|
+
if (faceCount > 1) {
|
|
353
|
+
return 'Multiple faces detected';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (livenessLevel === 0) {
|
|
357
|
+
if (faces.length === 0) return 'Position your face in the frame';
|
|
358
|
+
if (progress < 100) return 'Hold still...';
|
|
359
|
+
return 'Perfect! Capturing...';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (livenessLevel === 1) {
|
|
363
|
+
switch (livenessStep) {
|
|
364
|
+
case 0:
|
|
365
|
+
return 'Face the camera straight';
|
|
366
|
+
case 1:
|
|
367
|
+
return `Blink your eyes ${blinkCount} of 3 times`;
|
|
368
|
+
case 2:
|
|
369
|
+
return 'Perfect! Hold still — capturing...';
|
|
370
|
+
default:
|
|
371
|
+
return 'Align your face in frame';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (livenessLevel === 2) {
|
|
376
|
+
switch (livenessStep) {
|
|
377
|
+
case 0:
|
|
378
|
+
return 'Face the camera straight';
|
|
379
|
+
case 1:
|
|
380
|
+
return 'Turn your head to the LEFT';
|
|
381
|
+
case 2:
|
|
382
|
+
return 'Turn your head to the RIGHT';
|
|
383
|
+
case 3:
|
|
384
|
+
return 'Perfect! Hold still — capturing...';
|
|
385
|
+
default:
|
|
386
|
+
return 'Align your face in frame';
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (livenessLevel === 3) {
|
|
391
|
+
switch (livenessStep) {
|
|
392
|
+
case 0:
|
|
393
|
+
return 'Face the camera straight';
|
|
394
|
+
case 1:
|
|
395
|
+
return 'Turn your head to the LEFT';
|
|
396
|
+
case 2:
|
|
397
|
+
return 'Turn your head to the RIGHT';
|
|
398
|
+
case 3:
|
|
399
|
+
return `Blink your eyes ${blinkCount} of 3 times`;
|
|
400
|
+
case 4:
|
|
401
|
+
return 'Perfect! Hold still — capturing...';
|
|
402
|
+
default:
|
|
403
|
+
return 'Align your face in frame';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return 'Align your face in frame';
|
|
408
|
+
}, [livenessLevel, livenessStep, blinkCount, faces.length, progress, faceCount]);
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// Get step configuration based on liveness level
|
|
412
|
+
const getStepConfig = useCallback(() => {
|
|
413
|
+
switch (livenessLevel) {
|
|
414
|
+
case 0: // No liveness
|
|
415
|
+
return { totalSteps: 0, showSteps: false };
|
|
416
|
+
case 1: // Blink only
|
|
417
|
+
return { totalSteps: 1, showSteps: true };
|
|
418
|
+
case 2: // Face turn only
|
|
419
|
+
return { totalSteps: 2, showSteps: true };
|
|
420
|
+
case 3: // Both
|
|
421
|
+
return { totalSteps: 3, showSteps: true };
|
|
382
422
|
default:
|
|
383
|
-
return
|
|
423
|
+
return { totalSteps: 0, showSteps: false };
|
|
424
|
+
}
|
|
425
|
+
}, [livenessLevel]);
|
|
426
|
+
|
|
427
|
+
const stepConfig = getStepConfig();
|
|
428
|
+
|
|
429
|
+
// Get instruction text style based on content
|
|
430
|
+
const getInstructionStyle = useCallback(() => {
|
|
431
|
+
if (faceCount > 1) {
|
|
432
|
+
return [styles.instructionText, { color: Global.AppTheme.error }];
|
|
384
433
|
}
|
|
385
|
-
|
|
434
|
+
return styles.instructionText;
|
|
435
|
+
}, [faceCount]);
|
|
386
436
|
|
|
387
437
|
// camera placeholder / UI
|
|
388
438
|
const renderCameraPlaceholder = () => (
|
|
@@ -447,106 +497,149 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
447
497
|
!cameraError;
|
|
448
498
|
|
|
449
499
|
return (
|
|
450
|
-
<View style={styles.
|
|
451
|
-
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
500
|
+
<View style={styles.container}>
|
|
501
|
+
<View style={styles.cameraContainer}>
|
|
502
|
+
{shouldRenderCamera ? (
|
|
503
|
+
<Camera
|
|
504
|
+
ref={cameraRef}
|
|
505
|
+
style={styles.camera}
|
|
506
|
+
device={cameraDevice}
|
|
507
|
+
isActive={showCamera && !isLoading && appState.current === 'active'}
|
|
508
|
+
photo={true}
|
|
509
|
+
format={format}
|
|
510
|
+
codeScanner={showCodeScanner ? codeScanner : undefined}
|
|
511
|
+
frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
|
|
512
|
+
frameProcessorFps={frameProcessorFps}
|
|
513
|
+
onInitialized={handleCameraInitialized}
|
|
514
|
+
onError={handleCameraError}
|
|
515
|
+
enableZoomGesture={false}
|
|
516
|
+
exposure={0}
|
|
517
|
+
pixelFormat="yuv"
|
|
518
|
+
preset="medium"
|
|
519
|
+
orientation="portrait"
|
|
520
|
+
/>
|
|
521
|
+
) : (
|
|
522
|
+
renderCameraPlaceholder()
|
|
523
|
+
)}
|
|
473
524
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
525
|
+
{/* Liveness Detection UI */}
|
|
526
|
+
{!showCodeScanner && shouldRenderCamera && livenessLevel > 0 && (
|
|
527
|
+
<View style={styles.livenessContainer}>
|
|
528
|
+
{/* Instructions with animation */}
|
|
529
|
+
<Animated.View
|
|
530
|
+
style={[
|
|
531
|
+
styles.instructionContainer,
|
|
532
|
+
{
|
|
533
|
+
opacity: instructionAnim,
|
|
534
|
+
transform: [
|
|
535
|
+
{
|
|
536
|
+
translateY: instructionAnim.interpolate({
|
|
537
|
+
inputRange: [0, 1],
|
|
538
|
+
outputRange: [10, 0],
|
|
539
|
+
}),
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
]}
|
|
544
|
+
>
|
|
545
|
+
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
546
|
+
</Animated.View>
|
|
547
|
+
|
|
548
|
+
{/* Blink Progress for blink steps */}
|
|
549
|
+
{(livenessLevel === 1 || livenessLevel === 3) && livenessStep === (livenessLevel === 1 ? 1 : 3) && (
|
|
550
|
+
<View style={styles.blinkProgressContainer}>
|
|
551
|
+
{[1, 2, 3].map((i) => (
|
|
552
|
+
<View
|
|
553
|
+
key={i}
|
|
554
|
+
style={[
|
|
555
|
+
styles.blinkDot,
|
|
556
|
+
blinkCount >= i && styles.blinkDotActive,
|
|
557
|
+
]}
|
|
558
|
+
/>
|
|
559
|
+
))}
|
|
560
|
+
</View>
|
|
561
|
+
)}
|
|
562
|
+
|
|
563
|
+
{/* Step Indicators - only show for liveness levels 1, 2, 3 */}
|
|
564
|
+
{stepConfig.showSteps && faceCount <= 1 && (
|
|
565
|
+
<>
|
|
566
|
+
<View style={styles.stepsContainer}>
|
|
567
|
+
{Array.from({ length: stepConfig.totalSteps + 1 }).map((_, step) => (
|
|
568
|
+
<React.Fragment key={step}>
|
|
569
|
+
<View style={[
|
|
570
|
+
styles.stepIndicator,
|
|
571
|
+
livenessStep > step ? styles.stepCompleted :
|
|
572
|
+
livenessStep === step ? styles.stepCurrent : styles.stepPending
|
|
573
|
+
]}>
|
|
574
|
+
<Text style={styles.stepText}>{step + 1}</Text>
|
|
575
|
+
</View>
|
|
576
|
+
{step < stepConfig.totalSteps && (
|
|
577
|
+
<View style={[
|
|
578
|
+
styles.stepConnector,
|
|
579
|
+
livenessStep > step ? styles.connectorCompleted : {}
|
|
580
|
+
]} />
|
|
581
|
+
)}
|
|
582
|
+
</React.Fragment>
|
|
583
|
+
))}
|
|
507
584
|
</View>
|
|
508
|
-
{step < 2 && (
|
|
509
|
-
<View style={[
|
|
510
|
-
styles.stepConnector,
|
|
511
|
-
livenessStep > step ? styles.connectorCompleted : {}
|
|
512
|
-
]} />
|
|
513
|
-
)}
|
|
514
|
-
</React.Fragment>
|
|
515
|
-
))}
|
|
516
|
-
</View>
|
|
517
585
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
586
|
+
{/* Step Labels */}
|
|
587
|
+
<View style={styles.stepLabelsContainer}>
|
|
588
|
+
{livenessLevel === 1 && (
|
|
589
|
+
<>
|
|
590
|
+
<Text style={styles.stepLabel}>Center</Text>
|
|
591
|
+
<Text style={styles.stepLabel}>Blink</Text>
|
|
592
|
+
</>
|
|
593
|
+
)}
|
|
594
|
+
{livenessLevel === 2 && (
|
|
595
|
+
<>
|
|
596
|
+
<Text style={styles.stepLabel}>Center</Text>
|
|
597
|
+
<Text style={styles.stepLabel}>Left</Text>
|
|
598
|
+
<Text style={styles.stepLabel}>Right</Text>
|
|
599
|
+
</>
|
|
600
|
+
)}
|
|
601
|
+
{livenessLevel === 3 && (
|
|
602
|
+
<>
|
|
603
|
+
<Text style={styles.stepLabel}>Center</Text>
|
|
604
|
+
<Text style={styles.stepLabel}>Left</Text>
|
|
605
|
+
<Text style={styles.stepLabel}>Right</Text>
|
|
606
|
+
<Text style={styles.stepLabel}>Blink</Text>
|
|
607
|
+
</>
|
|
608
|
+
)}
|
|
609
|
+
</View>
|
|
610
|
+
</>
|
|
611
|
+
)}
|
|
523
612
|
</View>
|
|
524
|
-
|
|
525
|
-
)}
|
|
526
|
-
|
|
527
|
-
{/* Face detection status */}
|
|
528
|
-
{!showCodeScanner && shouldRenderCamera && faces.length > 1 && (
|
|
529
|
-
<View style={styles.faceDetectionStatus}>
|
|
530
|
-
<Text style={[styles.faceDetectionText, styles.multipleFacesText]}>
|
|
531
|
-
Multiple faces detected. Please ensure only one person is in the frame.
|
|
532
|
-
</Text>
|
|
533
|
-
</View>
|
|
534
|
-
)}
|
|
613
|
+
)}
|
|
535
614
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
615
|
+
{/* Simple face detection UI for liveness level 0 */}
|
|
616
|
+
{!showCodeScanner && shouldRenderCamera && livenessLevel === 0 && (
|
|
617
|
+
<View style={styles.livenessContainer}>
|
|
618
|
+
<Animated.View
|
|
619
|
+
style={[
|
|
620
|
+
styles.instructionContainer,
|
|
621
|
+
{
|
|
622
|
+
opacity: instructionAnim,
|
|
623
|
+
},
|
|
624
|
+
]}
|
|
625
|
+
>
|
|
626
|
+
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
627
|
+
</Animated.View>
|
|
628
|
+
|
|
629
|
+
{/* Progress bar for stability - only show when single face detected */}
|
|
630
|
+
{faceCount === 1 && (
|
|
631
|
+
<View style={styles.stabilityContainer}>
|
|
632
|
+
<View style={styles.stabilityBar}>
|
|
633
|
+
<View
|
|
634
|
+
style={[
|
|
635
|
+
styles.stabilityProgress,
|
|
636
|
+
{ width: `${progress}%` }
|
|
637
|
+
]}
|
|
638
|
+
/>
|
|
639
|
+
</View>
|
|
640
|
+
</View>
|
|
641
|
+
)}
|
|
642
|
+
</View>
|
|
550
643
|
)}
|
|
551
644
|
</View>
|
|
552
645
|
</View>
|
|
@@ -557,6 +650,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
557
650
|
CaptureImageWithoutEdit.displayName = 'CaptureImageWithoutEdit';
|
|
558
651
|
|
|
559
652
|
const styles = StyleSheet.create({
|
|
653
|
+
container: {
|
|
654
|
+
flex: 1,
|
|
655
|
+
width: '100%',
|
|
656
|
+
},
|
|
560
657
|
cameraContainer: {
|
|
561
658
|
flex: 1,
|
|
562
659
|
width: '100%',
|
|
@@ -569,6 +666,24 @@ const styles = StyleSheet.create({
|
|
|
569
666
|
flex: 1,
|
|
570
667
|
width: '100%',
|
|
571
668
|
},
|
|
669
|
+
controlsContainer: {
|
|
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
|
+
},
|
|
572
687
|
placeholderContainer: {
|
|
573
688
|
flex: 1,
|
|
574
689
|
justifyContent: 'center',
|
|
@@ -604,55 +719,6 @@ const styles = StyleSheet.create({
|
|
|
604
719
|
textAlign: 'center',
|
|
605
720
|
marginTop: 16,
|
|
606
721
|
},
|
|
607
|
-
cameraControls: {
|
|
608
|
-
position: 'absolute',
|
|
609
|
-
bottom: 30,
|
|
610
|
-
left: 0,
|
|
611
|
-
right: 0,
|
|
612
|
-
flexDirection: 'row',
|
|
613
|
-
justifyContent: 'center',
|
|
614
|
-
alignItems: 'center',
|
|
615
|
-
},
|
|
616
|
-
flipButton: {
|
|
617
|
-
position: 'absolute',
|
|
618
|
-
right: 30,
|
|
619
|
-
bottom: 40,
|
|
620
|
-
width: 55,
|
|
621
|
-
height: 55,
|
|
622
|
-
borderRadius: 27.5,
|
|
623
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
624
|
-
justifyContent: 'center',
|
|
625
|
-
alignItems: 'center',
|
|
626
|
-
shadowColor: '#000',
|
|
627
|
-
shadowOpacity: 0.3,
|
|
628
|
-
shadowOffset: { width: 0, height: 2 },
|
|
629
|
-
shadowRadius: 5,
|
|
630
|
-
elevation: 6,
|
|
631
|
-
},
|
|
632
|
-
flipButtonDisabled: {
|
|
633
|
-
opacity: 0.5,
|
|
634
|
-
},
|
|
635
|
-
faceDetectionStatus: {
|
|
636
|
-
position: 'absolute',
|
|
637
|
-
top: 40,
|
|
638
|
-
left: 0,
|
|
639
|
-
right: 0,
|
|
640
|
-
alignItems: 'center',
|
|
641
|
-
},
|
|
642
|
-
faceDetectionText: {
|
|
643
|
-
color: 'white',
|
|
644
|
-
fontSize: 16,
|
|
645
|
-
fontWeight: 'bold',
|
|
646
|
-
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
647
|
-
paddingHorizontal: 16,
|
|
648
|
-
paddingVertical: 8,
|
|
649
|
-
borderRadius: 8,
|
|
650
|
-
textAlign: 'center',
|
|
651
|
-
marginHorizontal: 20,
|
|
652
|
-
},
|
|
653
|
-
multipleFacesText: {
|
|
654
|
-
backgroundColor: 'rgba(255,0,0,0.7)',
|
|
655
|
-
},
|
|
656
722
|
// Liveness detection styles
|
|
657
723
|
livenessContainer: {
|
|
658
724
|
position: 'absolute',
|
|
@@ -667,7 +733,7 @@ const styles = StyleSheet.create({
|
|
|
667
733
|
paddingHorizontal: 20,
|
|
668
734
|
paddingVertical: 12,
|
|
669
735
|
borderRadius: 8,
|
|
670
|
-
marginBottom:
|
|
736
|
+
marginBottom: 10,
|
|
671
737
|
},
|
|
672
738
|
instructionText: {
|
|
673
739
|
color: 'white',
|
|
@@ -675,6 +741,20 @@ const styles = StyleSheet.create({
|
|
|
675
741
|
fontWeight: 'bold',
|
|
676
742
|
textAlign: 'center',
|
|
677
743
|
},
|
|
744
|
+
blinkProgressContainer: {
|
|
745
|
+
flexDirection: 'row',
|
|
746
|
+
marginVertical: 8,
|
|
747
|
+
},
|
|
748
|
+
blinkDot: {
|
|
749
|
+
width: 16,
|
|
750
|
+
height: 16,
|
|
751
|
+
borderRadius: 8,
|
|
752
|
+
backgroundColor: '#555',
|
|
753
|
+
marginHorizontal: 4,
|
|
754
|
+
},
|
|
755
|
+
blinkDotActive: {
|
|
756
|
+
backgroundColor: '#00ff88',
|
|
757
|
+
},
|
|
678
758
|
stepsContainer: {
|
|
679
759
|
flexDirection: 'row',
|
|
680
760
|
alignItems: 'center',
|
|
@@ -719,7 +799,7 @@ const styles = StyleSheet.create({
|
|
|
719
799
|
flexDirection: 'row',
|
|
720
800
|
justifyContent: 'space-between',
|
|
721
801
|
width: '100%',
|
|
722
|
-
paddingHorizontal:
|
|
802
|
+
paddingHorizontal: 5,
|
|
723
803
|
},
|
|
724
804
|
stepLabel: {
|
|
725
805
|
color: 'white',
|
|
@@ -728,6 +808,24 @@ const styles = StyleSheet.create({
|
|
|
728
808
|
textAlign: 'center',
|
|
729
809
|
flex: 1,
|
|
730
810
|
},
|
|
811
|
+
// Stability bar for level 0
|
|
812
|
+
stabilityContainer: {
|
|
813
|
+
alignItems: 'center',
|
|
814
|
+
width: '100%',
|
|
815
|
+
},
|
|
816
|
+
stabilityBar: {
|
|
817
|
+
width: '80%',
|
|
818
|
+
height: 6,
|
|
819
|
+
backgroundColor: 'rgba(255,255,255,0.3)',
|
|
820
|
+
borderRadius: 3,
|
|
821
|
+
overflow: 'hidden',
|
|
822
|
+
marginBottom: 8,
|
|
823
|
+
},
|
|
824
|
+
stabilityProgress: {
|
|
825
|
+
height: '100%',
|
|
826
|
+
backgroundColor: Global.AppTheme.primary,
|
|
827
|
+
borderRadius: 3,
|
|
828
|
+
},
|
|
731
829
|
});
|
|
732
830
|
|
|
733
831
|
export default CaptureImageWithoutEdit;
|