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.
@@ -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 { COLORS } from "../utils/constants";
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
- (async () => {
88
- try {
89
- if (!cameraRef.current) {
90
- throw new Error('Camera ref not available');
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
- if (!photo || !photo.path) {
101
- throw new Error('Failed to capture photo - no path returned');
102
- }
92
+ const photo = await cameraRef.current.takePhoto({
93
+ flash: 'off',
94
+ qualityPrioritization: 'quality',
95
+ enableShutterSound: false,
96
+ skipMetadata: true,
97
+ });
103
98
 
104
- const photopath = `file://${photo.path}`;
105
- const fileName = photopath.substr(photopath.lastIndexOf('/') + 1);
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, // Pass active state
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
- // Initialize camera
182
- const initializeCamera = useCallback(async () => {
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
- const permission = await Camera.requestCameraPermission();
194
- setCameraPermission(permission);
195
-
196
- if (permission === 'granted') {
188
+ if (newCameraPermission === 'granted') {
197
189
  const devices = await Camera.getAvailableCameraDevices();
198
-
199
- if (!devices || devices.length === 0) {
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
- throw new Error(`Camera permission ${permission}`);
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
- if (isMounted.current) {
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
- }, [cameraType]);
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
- // Render liveness instructions
366
- const renderLivenessInstruction = () => {
367
- if (faces.length === 0) return 'Position your face in the frame';
368
-
369
- switch (livenessStep) {
370
- case 0:
371
- return 'Look straight into the camera';
372
- case 1:
373
- return 'Turn your face to the right';
374
- case 2:
375
- return 'Now turn your face to the left';
376
- case 3:
377
- if (captured.current) {
378
- return 'Capturing...';
379
- } else {
380
- return 'Perfect! Hold still for capture...';
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 'Position your face in the frame';
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={COLORS.error} />
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={COLORS.light} />
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={COLORS.primary} />
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={COLORS.light} />
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={COLORS.primary} />
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.cameraContainer}>
451
- {shouldRenderCamera ? (
452
- <Camera
453
- ref={cameraRef}
454
- style={styles.camera}
455
- device={cameraDevice}
456
- isActive={showCamera && !isLoading && appState.current === 'active'}
457
- photo={true}
458
- format={format}
459
- codeScanner={showCodeScanner ? codeScanner : undefined}
460
- frameProcessor={!showCodeScanner && cameraInitialized ? frameProcessor : undefined}
461
- frameProcessorFps={frameProcessorFps}
462
- onInitialized={handleCameraInitialized}
463
- onError={handleCameraError}
464
- enableZoomGesture={false}
465
- exposure={0}
466
- pixelFormat="yuv"
467
- preset="medium"
468
- orientation="portrait"
469
- />
470
- ) : (
471
- renderCameraPlaceholder()
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
- {/* Liveness Detection UI */}
475
- {!showCodeScanner && shouldRenderCamera && (
476
- <View style={styles.livenessContainer}>
477
- {/* Instructions with animation */}
478
- <Animated.View
479
- style={[
480
- styles.instructionContainer,
481
- {
482
- opacity: instructionAnim,
483
- transform: [
484
- {
485
- translateY: instructionAnim.interpolate({
486
- inputRange: [0, 1],
487
- outputRange: [10, 0],
488
- }),
489
- },
490
- ],
491
- },
492
- ]}
493
- >
494
- <Text style={styles.instructionText}>{renderLivenessInstruction()}</Text>
495
- </Animated.View>
496
-
497
- {/* Step Indicators */}
498
- <View style={styles.stepsContainer}>
499
- {[0, 1, 2].map((step) => (
500
- <React.Fragment key={step}>
501
- <View style={[
502
- styles.stepIndicator,
503
- livenessStep > step ? styles.stepCompleted :
504
- livenessStep === step ? styles.stepCurrent : styles.stepPending
505
- ]}>
506
- <Text style={styles.stepText}>{step + 1}</Text>
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
- {/* Step Labels */}
519
- <View style={styles.stepLabelsContainer}>
520
- <Text style={styles.stepLabel}>Center</Text>
521
- <Text style={styles.stepLabel}>Right</Text>
522
- <Text style={styles.stepLabel}>Left</Text>
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
- </View>
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
- {/* Controls */}
537
- <View style={styles.cameraControls}>
538
- {onToggleCamera && currentStep === 'Identity Verification' && shouldRenderCamera && (
539
- <TouchableOpacity
540
- style={[
541
- styles.flipButton,
542
- (isLoading || captured.current || livenessStep > 0) && styles.flipButtonDisabled,
543
- ]}
544
- onPress={onToggleCamera}
545
- disabled={isLoading || captured.current || livenessStep > 0}
546
- accessibilityLabel="Flip camera"
547
- >
548
- <Icon name="flip-camera-ios" size={28} color={COLORS.light} />
549
- </TouchableOpacity>
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: COLORS.dark,
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: COLORS.error,
700
+ color: Global.AppTheme.error,
586
701
  fontSize: 16,
587
702
  textAlign: 'center',
588
703
  marginVertical: 16,
589
704
  },
590
705
  retryButton: {
591
- backgroundColor: COLORS.primary,
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: COLORS.light,
713
+ color: Global.AppTheme.light,
599
714
  fontWeight: 'bold',
600
715
  },
601
716
  placeholderText: {
602
- color: COLORS.light,
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: 20,
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: COLORS.primary,
693
- borderColor: COLORS.primary,
772
+ backgroundColor: Global.AppTheme.primary,
773
+ borderColor: Global.AppTheme.primary,
694
774
  },
695
775
  stepCurrent: {
696
- backgroundColor: COLORS.primary,
697
- borderColor: COLORS.primary,
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: COLORS.primary,
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: 10,
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;