react-native-biometric-verifier 0.0.25 → 0.0.26
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
|
@@ -24,11 +24,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
24
24
|
showCodeScanner = false,
|
|
25
25
|
isLoading = false,
|
|
26
26
|
frameProcessorFps = 1,
|
|
27
|
-
livenessLevel
|
|
27
|
+
livenessLevel,
|
|
28
28
|
}) => {
|
|
29
29
|
const cameraRef = useRef(null);
|
|
30
30
|
const [cameraDevice, setCameraDevice] = useState(null);
|
|
31
|
-
const [cameraPermission, setCameraPermission] = useState('not-determined');
|
|
32
31
|
const [showCamera, setShowCamera] = useState(false);
|
|
33
32
|
const [cameraInitialized, setCameraInitialized] = useState(false);
|
|
34
33
|
const [currentCameraType, setCurrentCameraType] = useState(cameraType);
|
|
@@ -161,7 +160,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
161
160
|
showCodeScanner,
|
|
162
161
|
isLoading,
|
|
163
162
|
isActive: showCamera && cameraInitialized,
|
|
164
|
-
livenessLevel,
|
|
163
|
+
livenessLevel: livenessLevel || 0, // Ensure livenessLevel is never null/undefined
|
|
165
164
|
});
|
|
166
165
|
|
|
167
166
|
useEffect(() => {
|
|
@@ -181,20 +180,22 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
181
180
|
setShowCamera(false);
|
|
182
181
|
|
|
183
182
|
const newCameraPermission = await Camera.requestCameraPermission();
|
|
184
|
-
setCameraPermission(newCameraPermission);
|
|
185
|
-
|
|
186
183
|
if (newCameraPermission === 'granted') {
|
|
187
|
-
|
|
184
|
+
let devices = await Camera.getAvailableCameraDevices();
|
|
185
|
+
|
|
186
|
+
// 🔁 Retry once after short delay if no devices found
|
|
187
|
+
if (!devices || devices.length === 0) {
|
|
188
|
+
console.warn('No camera devices yet, retrying in 300ms...');
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
190
|
+
devices = await Camera.getAvailableCameraDevices();
|
|
191
|
+
}
|
|
188
192
|
|
|
189
193
|
if (!devices || devices.length === 0) {
|
|
190
194
|
throw new Error('No camera devices available');
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
const device = getCameraDevice(devices, currentCameraType);
|
|
194
|
-
|
|
195
|
-
if (!device) {
|
|
196
|
-
throw new Error(`No ${currentCameraType} camera available`);
|
|
197
|
-
}
|
|
198
|
+
if (!device) throw new Error(`No ${currentCameraType} camera available`);
|
|
198
199
|
|
|
199
200
|
setCameraDevice(device);
|
|
200
201
|
setShowCamera(true);
|
|
@@ -213,6 +214,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
213
214
|
}
|
|
214
215
|
}, [currentCameraType]);
|
|
215
216
|
|
|
217
|
+
|
|
216
218
|
const initializeCamera = useCallback(async () => {
|
|
217
219
|
await getPermission();
|
|
218
220
|
}, [getPermission]);
|
|
@@ -289,13 +291,15 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
289
291
|
return 'Multiple faces detected';
|
|
290
292
|
}
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
const currentLevel = livenessLevel || 0;
|
|
295
|
+
|
|
296
|
+
if (currentLevel === 0) {
|
|
293
297
|
if (faces.length === 0) return 'Position your face in the frame';
|
|
294
298
|
if (progress < 100) return 'Hold still...';
|
|
295
299
|
return 'Perfect! Capturing...';
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
if (
|
|
302
|
+
if (currentLevel === 1) {
|
|
299
303
|
switch (livenessStep) {
|
|
300
304
|
case 0:
|
|
301
305
|
return 'Face the camera straight';
|
|
@@ -308,7 +312,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
308
312
|
}
|
|
309
313
|
}
|
|
310
314
|
|
|
311
|
-
if (
|
|
315
|
+
if (currentLevel === 2) {
|
|
312
316
|
switch (livenessStep) {
|
|
313
317
|
case 0:
|
|
314
318
|
return 'Face the camera straight';
|
|
@@ -323,7 +327,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
|
|
326
|
-
if (
|
|
330
|
+
if (currentLevel === 3) {
|
|
327
331
|
switch (livenessStep) {
|
|
328
332
|
case 0:
|
|
329
333
|
return 'Face the camera straight';
|
|
@@ -351,7 +355,9 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
351
355
|
]);
|
|
352
356
|
|
|
353
357
|
const getStepConfig = useCallback(() => {
|
|
354
|
-
|
|
358
|
+
const currentLevel = livenessLevel || 0;
|
|
359
|
+
|
|
360
|
+
switch (currentLevel) {
|
|
355
361
|
case 0:
|
|
356
362
|
return { totalSteps: 0, showSteps: false };
|
|
357
363
|
case 1:
|
|
@@ -377,13 +383,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
377
383
|
return (
|
|
378
384
|
<View style={styles.container}>
|
|
379
385
|
<View style={styles.cameraContainer}>
|
|
380
|
-
{isInitializing && (
|
|
381
|
-
<View style={styles.loadingContainer}>
|
|
382
|
-
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
383
|
-
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
384
|
-
</View>
|
|
385
|
-
)}
|
|
386
|
-
|
|
387
386
|
{!isInitializing && showCamera && cameraDevice ? (
|
|
388
387
|
<Camera
|
|
389
388
|
ref={cameraRef}
|
|
@@ -403,21 +402,16 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
403
402
|
}}
|
|
404
403
|
onError={(error) => {
|
|
405
404
|
console.log('Camera error:', error);
|
|
406
|
-
setCameraError('Camera error occurred');
|
|
407
|
-
setCameraInitialized(false);
|
|
408
405
|
}}
|
|
409
|
-
enableZoomGesture={false}
|
|
410
|
-
exposure={0}
|
|
411
|
-
pixelFormat="yuv"
|
|
412
|
-
preset="medium"
|
|
413
|
-
orientation="portrait"
|
|
414
406
|
/>
|
|
415
407
|
) : (
|
|
416
408
|
<View style={styles.placeholderContainer}>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
409
|
+
{isInitializing && (
|
|
410
|
+
<View style={styles.loadingContainer}>
|
|
411
|
+
<ActivityIndicator size="large" color={Global.AppTheme.primary} />
|
|
412
|
+
<Text style={styles.placeholderText}>Initializing camera...</Text>
|
|
413
|
+
</View>
|
|
414
|
+
)}
|
|
421
415
|
<TouchableOpacity
|
|
422
416
|
style={styles.retryButton}
|
|
423
417
|
onPress={handleRetry}
|
|
@@ -428,7 +422,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
428
422
|
</View>
|
|
429
423
|
)}
|
|
430
424
|
|
|
431
|
-
{!showCodeScanner && showCamera && cameraDevice && livenessLevel > 0 && (
|
|
425
|
+
{!showCodeScanner && showCamera && cameraDevice && (livenessLevel || 0) > 0 && (
|
|
432
426
|
<View style={styles.livenessContainer}>
|
|
433
427
|
<Animated.View
|
|
434
428
|
style={[
|
|
@@ -476,8 +470,8 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
476
470
|
livenessStep > step
|
|
477
471
|
? styles.stepCompleted
|
|
478
472
|
: livenessStep === step
|
|
479
|
-
|
|
480
|
-
|
|
473
|
+
? styles.stepCurrent
|
|
474
|
+
: styles.stepPending,
|
|
481
475
|
]}
|
|
482
476
|
>
|
|
483
477
|
<Text style={styles.stepText}>{step + 1}</Text>
|
|
@@ -525,7 +519,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
525
519
|
</View>
|
|
526
520
|
)}
|
|
527
521
|
|
|
528
|
-
{!showCodeScanner && showCamera && cameraDevice && livenessLevel === 0 && (
|
|
522
|
+
{!showCodeScanner && showCamera && cameraDevice && (livenessLevel || 0) === 0 && (
|
|
529
523
|
<View style={styles.livenessContainer}>
|
|
530
524
|
<Animated.View
|
|
531
525
|
style={[
|
package/src/components/Loader.js
CHANGED
|
@@ -10,35 +10,35 @@ import {
|
|
|
10
10
|
} from "react-native";
|
|
11
11
|
import FastImage from 'react-native-fast-image';
|
|
12
12
|
import { normalize } from "react-native-elements";
|
|
13
|
+
import { getLoaderGif } from "../utils/getLoaderGif";
|
|
13
14
|
|
|
14
15
|
export default function Loader({
|
|
15
|
-
|
|
16
|
+
state,
|
|
16
17
|
overlayColor = 'rgba(0,0,0,0.4)',
|
|
17
18
|
loaderColor = 'lightblue',
|
|
18
19
|
size = 50,
|
|
19
|
-
gifSource,
|
|
20
|
+
gifSource = { uri: "http://emr.amalaims.org:9393/file/getCommonFile/image/Face.gif" },
|
|
20
21
|
message = '',
|
|
21
22
|
messageStyle = {},
|
|
22
23
|
animationType = 'fade',
|
|
23
24
|
hasBackground = true,
|
|
24
25
|
borderRadius = 20,
|
|
25
|
-
shadow = true
|
|
26
|
+
shadow = true,
|
|
27
|
+
imageurl,
|
|
26
28
|
}) {
|
|
27
29
|
const [rotation] = useState(new Animated.Value(0));
|
|
28
30
|
const [pulse] = useState(new Animated.Value(1));
|
|
29
31
|
const [fade] = useState(new Animated.Value(0));
|
|
30
32
|
const [imageSource, setImageSource] = useState(gifSource);
|
|
31
|
-
|
|
33
|
+
const error = getLoaderGif(state.animationState, state.currentStep, "http://emr.amalaims.org:9393/", imageurl)
|
|
32
34
|
// Reset imageSource whenever gifSource prop changes
|
|
33
35
|
useEffect(() => {
|
|
34
36
|
setImageSource(gifSource);
|
|
35
37
|
}, [gifSource]);
|
|
36
38
|
|
|
37
39
|
const handleImageError = () => {
|
|
38
|
-
|
|
39
|
-
setImageSource(
|
|
40
|
-
uri: "http://emr.amalaims.org:9393/file/getCommonFile/image/heartpulse.gif"
|
|
41
|
-
});
|
|
40
|
+
console.log('LoaderError', JSON.stringify(error))
|
|
41
|
+
setImageSource(error);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// Rotation animation
|
|
@@ -92,8 +92,8 @@ export default function Loader({
|
|
|
92
92
|
/>
|
|
93
93
|
) : (
|
|
94
94
|
<Animated.View style={[
|
|
95
|
-
styles.defaultLoader,
|
|
96
|
-
{
|
|
95
|
+
styles.defaultLoader,
|
|
96
|
+
{
|
|
97
97
|
borderColor: loaderColor,
|
|
98
98
|
transform: [{ rotate: spin }, { scale: pulse }],
|
|
99
99
|
width: normalize(size),
|
|
@@ -102,8 +102,8 @@ export default function Loader({
|
|
|
102
102
|
}
|
|
103
103
|
]}>
|
|
104
104
|
<View style={[
|
|
105
|
-
styles.innerCircle,
|
|
106
|
-
{
|
|
105
|
+
styles.innerCircle,
|
|
106
|
+
{
|
|
107
107
|
backgroundColor: loaderColor,
|
|
108
108
|
width: normalize(size / 2),
|
|
109
109
|
height: normalize(size / 2)
|
|
@@ -116,19 +116,19 @@ export default function Loader({
|
|
|
116
116
|
<Modal
|
|
117
117
|
animationType={animationType}
|
|
118
118
|
transparent={true}
|
|
119
|
-
visible={
|
|
120
|
-
onRequestClose={() => {}}
|
|
119
|
+
visible={state.isLoading}
|
|
120
|
+
onRequestClose={() => { }}
|
|
121
121
|
>
|
|
122
122
|
<Animated.View style={[
|
|
123
123
|
styles.modalContainer,
|
|
124
|
-
{
|
|
124
|
+
{
|
|
125
125
|
backgroundColor: overlayColor,
|
|
126
126
|
opacity: fade
|
|
127
127
|
}
|
|
128
128
|
]}>
|
|
129
129
|
<Animated.View style={[
|
|
130
130
|
styles.loaderContainer,
|
|
131
|
-
{
|
|
131
|
+
{
|
|
132
132
|
backgroundColor: hasBackground ? 'white' : 'transparent',
|
|
133
133
|
borderRadius: normalize(borderRadius),
|
|
134
134
|
transform: [{ scale: pulse }],
|
|
@@ -6,7 +6,7 @@ import { useFaceDetector } from 'react-native-vision-camera-face-detector';
|
|
|
6
6
|
// Tuned constants for liveness detection / stability
|
|
7
7
|
const FACE_STABILITY_THRESHOLD = 3;
|
|
8
8
|
const FACE_MOVEMENT_THRESHOLD = 15;
|
|
9
|
-
const FRAME_PROCESSOR_MIN_INTERVAL_MS =
|
|
9
|
+
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 500;
|
|
10
10
|
const MIN_FACE_SIZE = 0.2;
|
|
11
11
|
|
|
12
12
|
// Liveness detection constants
|
|
@@ -19,19 +19,19 @@ const BLINK_THRESHOLD = 0.3; // Eye closed if below this
|
|
|
19
19
|
const REQUIRED_BLINKS = 3;
|
|
20
20
|
|
|
21
21
|
export const useFaceDetectionFrameProcessor = ({
|
|
22
|
-
onStableFaceDetected = () => {},
|
|
23
|
-
onFacesUpdate = () => {},
|
|
24
|
-
onLivenessUpdate = () => {},
|
|
22
|
+
onStableFaceDetected = () => { },
|
|
23
|
+
onFacesUpdate = () => { },
|
|
24
|
+
onLivenessUpdate = () => { },
|
|
25
25
|
showCodeScanner = false,
|
|
26
26
|
isLoading = false,
|
|
27
27
|
isActive = true,
|
|
28
|
-
livenessLevel
|
|
28
|
+
livenessLevel,
|
|
29
29
|
}) => {
|
|
30
30
|
const { detectFaces } = useFaceDetector({
|
|
31
31
|
performanceMode: 'fast',
|
|
32
32
|
landmarkMode: 'none',
|
|
33
33
|
contourMode: 'none',
|
|
34
|
-
classificationMode: livenessLevel === 1 || livenessLevel === 3 ? 'all' : 'none', // Only enable classification for blink detection
|
|
34
|
+
classificationMode: (livenessLevel === 1 || livenessLevel === 3) ? 'all' : 'none', // Only enable classification for blink detection
|
|
35
35
|
minFaceSize: MIN_FACE_SIZE,
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -49,7 +49,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
49
49
|
captured: false,
|
|
50
50
|
showCodeScanner: showCodeScanner,
|
|
51
51
|
isActive: isActive,
|
|
52
|
-
livenessLevel: livenessLevel,
|
|
52
|
+
livenessLevel: livenessLevel || 0, // Ensure default value
|
|
53
53
|
livenessStep: 0, // 0=center, 1=left/center, 2=right/blink, 3=blink/capture, 4=capture
|
|
54
54
|
leftTurnVerified: false,
|
|
55
55
|
rightTurnVerified: false,
|
|
@@ -66,7 +66,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
66
66
|
// update flags without recreating object
|
|
67
67
|
sharedState.value.showCodeScanner = !!showCodeScanner;
|
|
68
68
|
sharedState.value.isActive = !!isActive;
|
|
69
|
-
sharedState.value.livenessLevel = livenessLevel;
|
|
69
|
+
sharedState.value.livenessLevel = livenessLevel || 0;
|
|
70
70
|
|
|
71
71
|
if (isActive && sharedState.value.captured) {
|
|
72
72
|
// reset minimal fields
|
|
@@ -134,8 +134,9 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
134
134
|
const frameProcessor = useFrameProcessor(
|
|
135
135
|
(frame) => {
|
|
136
136
|
'worklet';
|
|
137
|
+
console.log('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq',JSON.stringify(frame))
|
|
137
138
|
const state = sharedState.value;
|
|
138
|
-
|
|
139
|
+
|
|
139
140
|
// quick exits — do not call frame.release here; we'll release in finally
|
|
140
141
|
if (state.showCodeScanner || state.captured || isLoading || !state.isActive) {
|
|
141
142
|
frame.release?.();
|
|
@@ -193,11 +194,13 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
193
194
|
} = state;
|
|
194
195
|
|
|
195
196
|
// ---- Liveness Logic Based on Level ----
|
|
197
|
+
// Level 0: Basic face detection with stability check only
|
|
196
198
|
if (livenessLevel === 0) {
|
|
197
199
|
// No liveness check - only stability
|
|
198
200
|
// Skip all liveness steps and go directly to capture when stable
|
|
199
201
|
livenessStep = 0;
|
|
200
|
-
}
|
|
202
|
+
}
|
|
203
|
+
// Level 1: Face detection with blink verification
|
|
201
204
|
else if (livenessLevel === 1) {
|
|
202
205
|
// Blink detection only
|
|
203
206
|
if (livenessStep === 0) {
|
|
@@ -348,13 +351,13 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
348
351
|
|
|
349
352
|
// Capture condition based on liveness level
|
|
350
353
|
let shouldCapture = false;
|
|
351
|
-
|
|
354
|
+
|
|
355
|
+
// Level 0: Basic face detection - capture when stable
|
|
352
356
|
if (livenessLevel === 0) {
|
|
353
|
-
// No liveness - capture when stable
|
|
354
357
|
shouldCapture = newStableCount >= FACE_STABILITY_THRESHOLD;
|
|
355
358
|
}
|
|
359
|
+
// Level 1: Blink verification - capture after blinks and stable
|
|
356
360
|
else if (livenessLevel === 1) {
|
|
357
|
-
// Blink only - capture after blinks and stable
|
|
358
361
|
shouldCapture = livenessStep === 2 && blinkCount >= REQUIRED_BLINKS && newStableCount >= FACE_STABILITY_THRESHOLD;
|
|
359
362
|
}
|
|
360
363
|
else if (livenessLevel === 2) {
|
|
@@ -381,7 +384,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
381
384
|
// protect worklet from throwing
|
|
382
385
|
try {
|
|
383
386
|
console.error('FrameProcessor worklet error:', err);
|
|
384
|
-
} catch (e) {}
|
|
387
|
+
} catch (e) { }
|
|
385
388
|
} finally {
|
|
386
389
|
// Always release the frame exactly once
|
|
387
390
|
frame.release?.();
|
package/src/index.js
CHANGED
|
@@ -294,7 +294,7 @@ const BiometricModal = React.memo(
|
|
|
294
294
|
body
|
|
295
295
|
);
|
|
296
296
|
|
|
297
|
-
console.log("📨 API Response:", response);
|
|
297
|
+
console.log("📨 API Response:", JSON.stringify(response));
|
|
298
298
|
|
|
299
299
|
if (response?.httpstatus === 200) {
|
|
300
300
|
console.log("✅ Face recognition successful");
|
|
@@ -326,9 +326,9 @@ const BiometricModal = React.memo(
|
|
|
326
326
|
}, 1200);
|
|
327
327
|
}
|
|
328
328
|
} else {
|
|
329
|
-
console.warn("⚠️ Face recognition failed:", response?.
|
|
329
|
+
console.warn("⚠️ Face recognition failed:", response?.httpstatus);
|
|
330
330
|
handleProcessError(
|
|
331
|
-
response?.data?.
|
|
331
|
+
response?.data?.message ||
|
|
332
332
|
"Face not recognized. Please try again."
|
|
333
333
|
);
|
|
334
334
|
}
|
|
@@ -543,13 +543,8 @@ const BiometricModal = React.memo(
|
|
|
543
543
|
}
|
|
544
544
|
}, [data, modalVisible, startProcess]);
|
|
545
545
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
() =>
|
|
549
|
-
state.isLoading &&
|
|
550
|
-
getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl),
|
|
551
|
-
[state.isLoading, state.animationState, state.currentStep, apiurl, imageurl]
|
|
552
|
-
);
|
|
546
|
+
|
|
547
|
+
|
|
553
548
|
|
|
554
549
|
// Determine if camera should be shown
|
|
555
550
|
const shouldShowCamera =
|
|
@@ -626,10 +621,9 @@ const BiometricModal = React.memo(
|
|
|
626
621
|
currentTime={countdown}
|
|
627
622
|
/>
|
|
628
623
|
</View>
|
|
629
|
-
|
|
630
624
|
<Loader
|
|
631
|
-
|
|
632
|
-
|
|
625
|
+
state={state}
|
|
626
|
+
gifSource={getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl)}
|
|
633
627
|
/>
|
|
634
628
|
</View>
|
|
635
629
|
</Modal>
|
package/src/utils/Global.js
CHANGED
|
@@ -6,11 +6,11 @@ import { Global } from "./Global";
|
|
|
6
6
|
* @param {string} currentStep - Current step of verification
|
|
7
7
|
* @returns {any} - Gif image source or null
|
|
8
8
|
*/
|
|
9
|
-
export const getLoaderGif = (animationState, currentStep,
|
|
9
|
+
export const getLoaderGif = (animationState, currentStep,apiurl,imageurl ='file/getCommonFile/image/') => {
|
|
10
10
|
const FaceGifUrl =
|
|
11
|
-
`${
|
|
11
|
+
`${apiurl}${imageurl}Face.gif`;
|
|
12
12
|
const LocationGifUrl =
|
|
13
|
-
`${
|
|
13
|
+
`${apiurl}${imageurl}Location.gif`;
|
|
14
14
|
|
|
15
15
|
if (
|
|
16
16
|
animationState === Global.AnimationStates.faceScan ||
|