react-native-biometric-verifier 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-biometric-verifier",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "A React Native module for biometric verification with face recognition and QR code scanning",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  "qr-code"
15
15
  ],
16
16
  "author": "PRAFULDAS M M",
17
- "license": "JESCON TECHNOLOGIES PVT",
17
+ "license": "JESCON TECHNOLOGIES PVT LTD",
18
18
  "peerDependencies": {
19
19
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
20
20
  "react-native": ">=0.60.0",
@@ -6,8 +6,8 @@ import {
6
6
  StyleSheet,
7
7
  ActivityIndicator,
8
8
  Animated,
9
+ Dimensions,
9
10
  } from 'react-native';
10
- import Icon from 'react-native-vector-icons/MaterialIcons';
11
11
  import {
12
12
  Camera,
13
13
  getCameraDevice,
@@ -24,7 +24,7 @@ const CaptureImageWithoutEdit = React.memo(
24
24
  showCodeScanner = false,
25
25
  isLoading = false,
26
26
  frameProcessorFps = 1,
27
- livenessLevel,
27
+ livenessLevel = 0, // 0 = anti-spoof only, 1 = anti-spoof + blinking
28
28
  }) => {
29
29
  const cameraRef = useRef(null);
30
30
  const [cameraDevice, setCameraDevice] = useState(null);
@@ -38,11 +38,16 @@ const CaptureImageWithoutEdit = React.memo(
38
38
  const [blinkCount, setBlinkCount] = useState(0);
39
39
  const [progress, setProgress] = useState(0);
40
40
  const [faceCount, setFaceCount] = useState(0);
41
+ const [isFaceLive, setIsFaceLive] = useState(false);
42
+ const [antiSpoofConfidence, setAntiSpoofConfidence] = useState(0);
43
+ const [isFaceCentered, setIsFaceCentered] = useState(false);
44
+ const [hasSingleFace, setHasSingleFace] = useState(false);
41
45
 
42
46
  const captured = useRef(false);
43
47
  const isMounted = useRef(true);
44
48
 
45
49
  const instructionAnim = useRef(new Animated.Value(1)).current;
50
+ const liveIndicatorAnim = useRef(new Animated.Value(0)).current;
46
51
 
47
52
  const resetCaptureState = useCallback(() => {
48
53
  captured.current = false;
@@ -51,6 +56,10 @@ const CaptureImageWithoutEdit = React.memo(
51
56
  setBlinkCount(0);
52
57
  setProgress(0);
53
58
  setFaceCount(0);
59
+ setIsFaceLive(false);
60
+ setAntiSpoofConfidence(0);
61
+ setIsFaceCentered(false);
62
+ setHasSingleFace(false);
54
63
  }, []);
55
64
 
56
65
  const codeScanner = useCodeScanner({
@@ -112,10 +121,18 @@ const CaptureImageWithoutEdit = React.memo(
112
121
  const onFacesUpdate = useCallback((payload) => {
113
122
  if (!isMounted.current) return;
114
123
  try {
115
- const { count, progress } = payload;
124
+ const { count, progress, antiSpoofState } = payload;
116
125
  setFaceCount(count);
117
126
  setProgress(progress);
118
127
 
128
+ // Update anti-spoof related states
129
+ if (antiSpoofState) {
130
+ setIsFaceLive(antiSpoofState.isLive || false);
131
+ setAntiSpoofConfidence(antiSpoofState.confidence || 0);
132
+ setIsFaceCentered(antiSpoofState.isFaceCentered || false);
133
+ setHasSingleFace(antiSpoofState.hasSingleFace || false);
134
+ }
135
+
119
136
  if (count === 1) {
120
137
  setFaces((prev) => {
121
138
  if (prev.length === 1) return prev;
@@ -144,6 +161,33 @@ const CaptureImageWithoutEdit = React.memo(
144
161
  [instructionAnim]
145
162
  );
146
163
 
164
+ const onAntiSpoofUpdate = useCallback((result) => {
165
+ if (!isMounted.current) return;
166
+ try {
167
+ // Animate live indicator when face becomes live
168
+ if (result?.isLive && !isFaceLive) {
169
+ Animated.spring(liveIndicatorAnim, {
170
+ toValue: 1,
171
+ tension: 50,
172
+ friction: 7,
173
+ useNativeDriver: true,
174
+ }).start();
175
+ } else if (!result?.isLive && isFaceLive) {
176
+ Animated.timing(liveIndicatorAnim, {
177
+ toValue: 0,
178
+ duration: 200,
179
+ useNativeDriver: true,
180
+ }).start();
181
+ }
182
+
183
+ setIsFaceLive(result?.isLive || false);
184
+ setAntiSpoofConfidence(result?.confidence || 0);
185
+ setIsFaceCentered(result?.isFaceCentered || false);
186
+ } catch (error) {
187
+ console.error('Error updating anti-spoof:', error);
188
+ }
189
+ }, [isFaceLive, liveIndicatorAnim]);
190
+
147
191
  const {
148
192
  frameProcessor,
149
193
  forceResetCaptureState,
@@ -154,10 +198,11 @@ const CaptureImageWithoutEdit = React.memo(
154
198
  onStableFaceDetected,
155
199
  onFacesUpdate,
156
200
  onLivenessUpdate,
201
+ onAntiSpoofUpdate,
157
202
  showCodeScanner,
158
203
  isLoading,
159
204
  isActive: showCamera && cameraInitialized,
160
- livenessLevel: livenessLevel || 0, // Ensure livenessLevel is never null/undefined
205
+ livenessLevel: livenessLevel,
161
206
  });
162
207
 
163
208
  useEffect(() => {
@@ -197,6 +242,7 @@ const CaptureImageWithoutEdit = React.memo(
197
242
  setShowCamera(true);
198
243
  console.log('Camera device set successfully');
199
244
  } else {
245
+ console.warn('Camera permission not granted');
200
246
  }
201
247
  } catch (error) {
202
248
  console.error('Camera permission error:', error);
@@ -208,7 +254,6 @@ const CaptureImageWithoutEdit = React.memo(
208
254
  }
209
255
  }, [currentCameraType]);
210
256
 
211
-
212
257
  const initializeCamera = useCallback(async () => {
213
258
  await getPermission();
214
259
  }, [getPermission]);
@@ -245,7 +290,6 @@ const CaptureImageWithoutEdit = React.memo(
245
290
  }, [cameraType, currentCameraType, initializeCamera]);
246
291
 
247
292
  const format = useCameraFormat(cameraDevice, [
248
- { videoResolution: { width: 640, height: 640 } },
249
293
  { fps: 30 },
250
294
  ]);
251
295
 
@@ -283,54 +327,31 @@ const CaptureImageWithoutEdit = React.memo(
283
327
  return 'Multiple faces detected';
284
328
  }
285
329
 
286
- const currentLevel = livenessLevel || 0;
287
-
288
- if (currentLevel === 0) {
289
- if (faces.length === 0) return 'Position your face in the frame';
290
- if (progress < 100) return 'Hold still...';
291
- return 'Perfect! Capturing...';
330
+ if (!hasSingleFace) {
331
+ return 'Position your face in the frame';
292
332
  }
293
333
 
294
- if (currentLevel === 1) {
295
- switch (livenessStep) {
296
- case 0:
297
- return 'Face the camera straight';
298
- case 1:
299
- return `Blink your eyes ${blinkCount} of 3 times`;
300
- case 2:
301
- return 'Perfect! Hold still — capturing...';
302
- default:
303
- return 'Align your face in frame';
304
- }
334
+ if (!isFaceCentered) {
335
+ return 'Center your face in the frame';
305
336
  }
306
337
 
307
- if (currentLevel === 2) {
308
- switch (livenessStep) {
309
- case 0:
310
- return 'Face the camera straight';
311
- case 1:
312
- return 'Turn your head to the RIGHT';
313
- case 2:
314
- return 'Turn your head to the LEFT';
315
- case 3:
316
- return 'Perfect! Hold still — capturing...';
317
- default:
318
- return 'Align your face in frame';
319
- }
338
+ if (livenessLevel === 0) {
339
+ if (!isFaceLive) return 'Verifying liveness...';
340
+ if (progress < 100) return 'Hold still...';
341
+ return 'Perfect! Capturing...';
320
342
  }
321
343
 
322
- if (currentLevel === 3) {
344
+ if (livenessLevel === 1) {
323
345
  switch (livenessStep) {
324
346
  case 0:
325
347
  return 'Face the camera straight';
326
348
  case 1:
327
- return 'Turn your head to the RIGHT';
328
- case 2:
329
- return 'Turn your head to the LEFT';
330
- case 3:
349
+ if (!isFaceLive) return 'Verifying liveness...';
331
350
  return `Blink your eyes ${blinkCount} of 3 times`;
332
- case 4:
333
- return 'Perfect! Hold still — capturing...';
351
+ case 2:
352
+ if (!isFaceLive) return 'Verifying liveness...';
353
+ if (progress < 100) return 'Hold still...';
354
+ return 'Perfect! Capturing...';
334
355
  default:
335
356
  return 'Align your face in frame';
336
357
  }
@@ -341,23 +362,62 @@ const CaptureImageWithoutEdit = React.memo(
341
362
  livenessLevel,
342
363
  livenessStep,
343
364
  blinkCount,
344
- faces.length,
345
365
  progress,
346
366
  faceCount,
367
+ hasSingleFace,
368
+ isFaceCentered,
369
+ isFaceLive,
347
370
  ]);
348
371
 
349
- const getStepConfig = useCallback(() => {
350
- const currentLevel = livenessLevel || 0;
372
+ const getInstructionContainerStyle = useCallback(() => {
373
+ const baseStyle = [
374
+ styles.instructionContainer,
375
+ {
376
+ opacity: instructionAnim,
377
+ transform: [
378
+ {
379
+ translateY: instructionAnim.interpolate({
380
+ inputRange: [0, 1],
381
+ outputRange: [10, 0],
382
+ }),
383
+ },
384
+ ],
385
+ },
386
+ ];
387
+
388
+ if (faceCount > 1) {
389
+ return [...baseStyle, styles.errorInstructionContainer];
390
+ }
391
+
392
+ if (isFaceLive) {
393
+ return [...baseStyle, styles.liveInstructionContainer];
394
+ }
395
+
396
+ if (hasSingleFace && isFaceCentered) {
397
+ return [...baseStyle, styles.verifyingInstructionContainer];
398
+ }
399
+
400
+ return baseStyle;
401
+ }, [faceCount, isFaceLive, hasSingleFace, isFaceCentered, instructionAnim]);
402
+
403
+ const getInstructionStyle = useCallback(() => {
404
+ if (faceCount > 1) {
405
+ return [styles.instructionText, styles.errorInstructionText];
406
+ }
407
+
408
+ if (isFaceLive) {
409
+ return [styles.instructionText, styles.liveInstructionText];
410
+ }
411
+
412
+ return styles.instructionText;
413
+ }, [faceCount, isFaceLive]);
351
414
 
352
- switch (currentLevel) {
415
+ const getStepConfig = useCallback(() => {
416
+ switch (livenessLevel) {
353
417
  case 0:
354
418
  return { totalSteps: 0, showSteps: false };
355
419
  case 1:
356
420
  return { totalSteps: 1, showSteps: true };
357
- case 2:
358
- return { totalSteps: 2, showSteps: true };
359
- case 3:
360
- return { totalSteps: 3, showSteps: true };
361
421
  default:
362
422
  return { totalSteps: 0, showSteps: false };
363
423
  }
@@ -365,13 +425,6 @@ const CaptureImageWithoutEdit = React.memo(
365
425
 
366
426
  const stepConfig = getStepConfig();
367
427
 
368
- const getInstructionStyle = useCallback(() => {
369
- if (faceCount > 1) {
370
- return [styles.instructionText, { color: Global.AppTheme.error }];
371
- }
372
- return styles.instructionText;
373
- }, [faceCount]);
374
-
375
428
  return (
376
429
  <View style={styles.container}>
377
430
  <View style={styles.cameraContainer}>
@@ -420,41 +473,25 @@ const CaptureImageWithoutEdit = React.memo(
420
473
  </View>
421
474
  )}
422
475
 
423
- {!showCodeScanner && showCamera && cameraDevice && (livenessLevel || 0) > 0 && (
476
+ {!showCodeScanner && showCamera && cameraDevice && livenessLevel === 1 && (
424
477
  <View style={styles.livenessContainer}>
425
- <Animated.View
426
- style={[
427
- styles.instructionContainer,
428
- {
429
- opacity: instructionAnim,
430
- transform: [
431
- {
432
- translateY: instructionAnim.interpolate({
433
- inputRange: [0, 1],
434
- outputRange: [10, 0],
435
- }),
436
- },
437
- ],
438
- },
439
- ]}
440
- >
478
+ <Animated.View style={getInstructionContainerStyle()}>
441
479
  <Text style={getInstructionStyle()}>{getInstruction()}</Text>
442
480
  </Animated.View>
443
481
 
444
- {(livenessLevel === 1 || livenessLevel === 3) &&
445
- livenessStep === (livenessLevel === 1 ? 1 : 3) && (
446
- <View style={styles.blinkProgressContainer}>
447
- {[1, 2, 3].map((i) => (
448
- <View
449
- key={i}
450
- style={[
451
- styles.blinkDot,
452
- blinkCount >= i && styles.blinkDotActive,
453
- ]}
454
- />
455
- ))}
456
- </View>
457
- )}
482
+ {livenessStep === 1 && (
483
+ <View style={styles.blinkProgressContainer}>
484
+ {[1, 2, 3].map((i) => (
485
+ <View
486
+ key={i}
487
+ style={[
488
+ styles.blinkDot,
489
+ blinkCount >= i && styles.blinkDotActive,
490
+ ]}
491
+ />
492
+ ))}
493
+ </View>
494
+ )}
458
495
 
459
496
  {stepConfig.showSteps && faceCount <= 1 && (
460
497
  <>
@@ -496,47 +533,34 @@ const CaptureImageWithoutEdit = React.memo(
496
533
  <Text style={styles.stepLabel}>Blink</Text>
497
534
  </>
498
535
  )}
499
- {livenessLevel === 2 && (
500
- <>
501
- <Text style={styles.stepLabel}>Center</Text>
502
- <Text style={styles.stepLabel}>Right</Text>
503
- <Text style={styles.stepLabel}>Left</Text>
504
- </>
505
- )}
506
- {livenessLevel === 3 && (
507
- <>
508
- <Text style={styles.stepLabel}>Center</Text>
509
- <Text style={styles.stepLabel}>Right</Text>
510
- <Text style={styles.stepLabel}>Left</Text>
511
- <Text style={styles.stepLabel}>Blink</Text>
512
- </>
513
- )}
514
536
  </View>
515
537
  </>
516
538
  )}
517
539
  </View>
518
540
  )}
519
541
 
520
- {!showCodeScanner && showCamera && cameraDevice && (livenessLevel || 0) === 0 && (
542
+ {!showCodeScanner && showCamera && cameraDevice && livenessLevel === 0 && (
521
543
  <View style={styles.livenessContainer}>
522
- <Animated.View
523
- style={[
524
- styles.instructionContainer,
525
- {
526
- opacity: instructionAnim,
527
- },
528
- ]}
529
- >
544
+ <Animated.View style={getInstructionContainerStyle()}>
530
545
  <Text style={getInstructionStyle()}>{getInstruction()}</Text>
531
546
  </Animated.View>
532
-
533
- {faceCount === 1 && (
534
- <View style={styles.stabilityContainer}>
535
- <View style={styles.stabilityBar}>
547
+ {isFaceCentered && (
548
+ <View style={styles.confidenceContainer}>
549
+ <Text style={styles.confidenceText}>
550
+ Confidence: {Math.round(antiSpoofConfidence * 100)}%
551
+ </Text>
552
+ <View style={styles.confidenceBar}>
536
553
  <View
537
554
  style={[
538
- styles.stabilityProgress,
539
- { width: `${progress}%` },
555
+ styles.confidenceProgress,
556
+ {
557
+ width: `${antiSpoofConfidence * 100}%`,
558
+ backgroundColor: antiSpoofConfidence > 0.7
559
+ ? Global.AppTheme.success
560
+ : antiSpoofConfidence > 0.4
561
+ ? Global.AppTheme.warning
562
+ : Global.AppTheme.error
563
+ }
540
564
  ]}
541
565
  />
542
566
  </View>
@@ -613,12 +637,130 @@ const styles = StyleSheet.create({
613
637
  borderRadius: 8,
614
638
  marginBottom: 10,
615
639
  },
640
+ liveInstructionContainer: {
641
+ backgroundColor: Global.AppTheme.success,
642
+ paddingHorizontal: 20,
643
+ paddingVertical: 12,
644
+ borderRadius: 8,
645
+ marginBottom: 10,
646
+ },
647
+ verifyingInstructionContainer: {
648
+ backgroundColor: Global.AppTheme.warning,
649
+ paddingHorizontal: 20,
650
+ paddingVertical: 12,
651
+ borderRadius: 8,
652
+ marginBottom: 10,
653
+ },
654
+ errorInstructionContainer: {
655
+ backgroundColor: Global.AppTheme.error,
656
+ paddingHorizontal: 20,
657
+ paddingVertical: 12,
658
+ borderRadius: 8,
659
+ marginBottom: 10,
660
+ },
616
661
  instructionText: {
617
662
  color: 'white',
618
663
  fontSize: 16,
619
664
  fontWeight: 'bold',
620
665
  textAlign: 'center',
621
666
  },
667
+ liveInstructionText: {
668
+ color: 'white',
669
+ fontWeight: 'bold',
670
+ },
671
+ errorInstructionText: {
672
+ color: 'white',
673
+ fontWeight: 'bold',
674
+ },
675
+ // Live Indicator
676
+ liveIndicator: {
677
+ position: 'absolute',
678
+ top: 20,
679
+ right: 20,
680
+ backgroundColor: Global.AppTheme.success,
681
+ paddingHorizontal: 12,
682
+ paddingVertical: 6,
683
+ borderRadius: 16,
684
+ shadowColor: '#000',
685
+ shadowOffset: {
686
+ width: 0,
687
+ height: 2,
688
+ },
689
+ shadowOpacity: 0.25,
690
+ shadowRadius: 3.84,
691
+ elevation: 5,
692
+ },
693
+ liveIndicatorInner: {
694
+ flexDirection: 'row',
695
+ alignItems: 'center',
696
+ },
697
+ livePulse: {
698
+ width: 8,
699
+ height: 8,
700
+ borderRadius: 4,
701
+ backgroundColor: 'white',
702
+ marginRight: 6,
703
+ },
704
+ liveIndicatorText: {
705
+ color: 'white',
706
+ fontSize: 12,
707
+ fontWeight: 'bold',
708
+ },
709
+ // Status Overview
710
+ statusOverview: {
711
+ position: 'absolute',
712
+ bottom: 20,
713
+ left: 20,
714
+ right: 20,
715
+ backgroundColor: 'rgba(0,0,0,0.8)',
716
+ borderRadius: 12,
717
+ padding: 16,
718
+ },
719
+ statusRow: {
720
+ flexDirection: 'row',
721
+ justifyContent: 'space-between',
722
+ marginBottom: 12,
723
+ },
724
+ statusItem: {
725
+ flexDirection: 'row',
726
+ alignItems: 'center',
727
+ },
728
+ statusDot: {
729
+ width: 10,
730
+ height: 10,
731
+ borderRadius: 5,
732
+ marginRight: 6,
733
+ },
734
+ statusGood: {
735
+ backgroundColor: Global.AppTheme.success,
736
+ },
737
+ statusPending: {
738
+ backgroundColor: '#666',
739
+ },
740
+ statusText: {
741
+ color: 'white',
742
+ fontSize: 12,
743
+ },
744
+ confidenceContainer: {
745
+ marginTop: 8,
746
+ },
747
+ confidenceText: {
748
+ color: 'white',
749
+ fontSize: 12,
750
+ marginBottom: 4,
751
+ textAlign: 'center',
752
+ },
753
+ confidenceBar: {
754
+ height: 4,
755
+ backgroundColor: 'rgba(255,255,255,0.3)',
756
+ borderRadius: 2,
757
+ overflow: 'hidden',
758
+ },
759
+ confidenceProgress: {
760
+ height: '100%',
761
+ borderRadius: 2,
762
+ },
763
+ // Existing styles
622
764
  blinkProgressContainer: {
623
765
  flexDirection: 'row',
624
766
  marginVertical: 8,
@@ -631,7 +773,7 @@ const styles = StyleSheet.create({
631
773
  marginHorizontal: 4,
632
774
  },
633
775
  blinkDotActive: {
634
- backgroundColor: '#00ff88',
776
+ backgroundColor: Global.AppTheme.success,
635
777
  },
636
778
  stepsContainer: {
637
779
  flexDirection: 'row',