react-native-biometric-verifier 0.0.18 → 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 +373 -275
- package/src/components/Card.js +6 -6
- package/src/components/CountdownTimer.js +3 -3
- package/src/components/Notification.js +6 -6
- package/src/components/StepIndicator.js +7 -7
- package/src/hooks/useCountdown.js +7 -7
- package/src/hooks/useFaceDetectionFrameProcessor.js +378 -261
- package/src/hooks/useImageProcessing.js +6 -6
- package/src/hooks/useNotifyMessage.js +4 -4
- package/src/index.js +32 -49
- package/src/utils/Global.js +48 -0
- package/src/utils/getLoaderGif.js +3 -3
- package/src/utils/constants.js +0 -73
|
@@ -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,
|
|
@@ -18,35 +17,35 @@ import {
|
|
|
18
17
|
useCameraFormat,
|
|
19
18
|
CameraRuntimeError,
|
|
20
19
|
} from 'react-native-vision-camera';
|
|
21
|
-
import {
|
|
20
|
+
import { Global } from '../utils/Global';
|
|
22
21
|
import { useFaceDetectionFrameProcessor } from '../hooks/useFaceDetectionFrameProcessor';
|
|
23
22
|
|
|
24
23
|
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,34 +345,101 @@ 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 = () => (
|
|
389
439
|
<View style={styles.cameraContainer}>
|
|
390
440
|
{cameraError ? (
|
|
391
441
|
<View style={styles.errorContainer}>
|
|
392
|
-
<Icon name="error-outline" size={40} color={
|
|
442
|
+
<Icon name="error-outline" size={40} color={Global.AppTheme.error} />
|
|
393
443
|
<Text style={styles.errorText}>{cameraError}</Text>
|
|
394
444
|
<TouchableOpacity
|
|
395
445
|
style={styles.retryButton}
|
|
@@ -401,7 +451,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
401
451
|
</View>
|
|
402
452
|
) : cameraPermission === 'denied' || cameraPermission === 'restricted' ? (
|
|
403
453
|
<View style={styles.placeholderContainer}>
|
|
404
|
-
<Icon name="camera-off" size={40} color={
|
|
454
|
+
<Icon name="camera-off" size={40} color={Global.AppTheme.light} />
|
|
405
455
|
<Text style={styles.placeholderText}>
|
|
406
456
|
Camera permission required. Please enable in settings.
|
|
407
457
|
</Text>
|
|
@@ -415,14 +465,14 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
415
465
|
</View>
|
|
416
466
|
) : cameraPermission === 'not-determined' ? (
|
|
417
467
|
<View style={styles.placeholderContainer}>
|
|
418
|
-
<ActivityIndicator size="large" color={
|
|
468
|
+
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
419
469
|
<Text style={styles.placeholderText}>
|
|
420
470
|
Requesting camera access...
|
|
421
471
|
</Text>
|
|
422
472
|
</View>
|
|
423
473
|
) : !cameraDevice ? (
|
|
424
474
|
<View style={styles.placeholderContainer}>
|
|
425
|
-
<Icon name="camera-alt" size={40} color={
|
|
475
|
+
<Icon name="camera-alt" size={40} color={Global.AppTheme.light} />
|
|
426
476
|
<Text style={styles.placeholderText}>Camera not available</Text>
|
|
427
477
|
<TouchableOpacity
|
|
428
478
|
style={styles.retryButton}
|
|
@@ -434,7 +484,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
434
484
|
</View>
|
|
435
485
|
) : (
|
|
436
486
|
<View style={styles.placeholderContainer}>
|
|
437
|
-
<ActivityIndicator size="large" color={
|
|
487
|
+
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
438
488
|
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
439
489
|
</View>
|
|
440
490
|
)}
|
|
@@ -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,18 +650,40 @@ 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%',
|
|
563
660
|
borderRadius: 12,
|
|
564
661
|
overflow: 'hidden',
|
|
565
|
-
backgroundColor:
|
|
662
|
+
backgroundColor: Global.AppTheme.dark,
|
|
566
663
|
minHeight: 300,
|
|
567
664
|
},
|
|
568
665
|
camera: {
|
|
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',
|
|
@@ -582,77 +697,28 @@ const styles = StyleSheet.create({
|
|
|
582
697
|
padding: 20,
|
|
583
698
|
},
|
|
584
699
|
errorText: {
|
|
585
|
-
color:
|
|
700
|
+
color: Global.AppTheme.error,
|
|
586
701
|
fontSize: 16,
|
|
587
702
|
textAlign: 'center',
|
|
588
703
|
marginVertical: 16,
|
|
589
704
|
},
|
|
590
705
|
retryButton: {
|
|
591
|
-
backgroundColor:
|
|
706
|
+
backgroundColor: Global.AppTheme.primary,
|
|
592
707
|
paddingHorizontal: 20,
|
|
593
708
|
paddingVertical: 10,
|
|
594
709
|
borderRadius: 8,
|
|
595
710
|
marginTop: 10,
|
|
596
711
|
},
|
|
597
712
|
retryButtonText: {
|
|
598
|
-
color:
|
|
713
|
+
color: Global.AppTheme.light,
|
|
599
714
|
fontWeight: 'bold',
|
|
600
715
|
},
|
|
601
716
|
placeholderText: {
|
|
602
|
-
color:
|
|
717
|
+
color: Global.AppTheme.light,
|
|
603
718
|
fontSize: 16,
|
|
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',
|
|
@@ -689,12 +769,12 @@ const styles = StyleSheet.create({
|
|
|
689
769
|
borderWidth: 2,
|
|
690
770
|
},
|
|
691
771
|
stepCompleted: {
|
|
692
|
-
backgroundColor:
|
|
693
|
-
borderColor:
|
|
772
|
+
backgroundColor: Global.AppTheme.primary,
|
|
773
|
+
borderColor: Global.AppTheme.primary,
|
|
694
774
|
},
|
|
695
775
|
stepCurrent: {
|
|
696
|
-
backgroundColor:
|
|
697
|
-
borderColor:
|
|
776
|
+
backgroundColor: Global.AppTheme.primary,
|
|
777
|
+
borderColor: Global.AppTheme.primary,
|
|
698
778
|
opacity: 0.7,
|
|
699
779
|
},
|
|
700
780
|
stepPending: {
|
|
@@ -713,13 +793,13 @@ const styles = StyleSheet.create({
|
|
|
713
793
|
marginHorizontal: 4,
|
|
714
794
|
},
|
|
715
795
|
connectorCompleted: {
|
|
716
|
-
backgroundColor:
|
|
796
|
+
backgroundColor: Global.AppTheme.primary,
|
|
717
797
|
},
|
|
718
798
|
stepLabelsContainer: {
|
|
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;
|