react-native-biometric-verifier 0.0.40 → 0.0.42
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 +16 -4
- package/src/components/Loader.js +96 -84
- package/src/hooks/useCountdown.js +34 -34
- package/src/hooks/useFaceDetectionFrameProcessor.js +20 -42
- package/src/index.js +27 -9
- package/src/utils/Global.js +22 -2
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-biometric-verifier",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"description": "A React Native module for biometric verification with face recognition and QR code scanning",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "JESCON TECHNOLOGIES PVT LMT ,THRISSUR,KERALA",
|
|
8
|
+
"author": "PRAFUL DAS M M",
|
|
6
9
|
"scripts": {
|
|
7
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
11
|
},
|
|
@@ -13,15 +16,24 @@
|
|
|
13
16
|
"face-recognition",
|
|
14
17
|
"qr-code"
|
|
15
18
|
],
|
|
16
|
-
"
|
|
17
|
-
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/jescon-technologies/react-native-biometric-verifier"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/jescon-technologies/react-native-biometric-verifier/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/jescon-technologies/react-native-biometric-verifier#readme",
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
18
31
|
"peerDependencies": {
|
|
19
32
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
|
20
33
|
"react-native": ">=0.60.0",
|
|
21
34
|
"react-native-vector-icons": "^9.0.0",
|
|
22
35
|
"react-native-geolocation-service": "^5.0.0",
|
|
23
36
|
"react-native-image-resizer": "^1.0.0",
|
|
24
|
-
"@react-navigation/native": "^6.0.0",
|
|
25
37
|
"prop-types": "^15.8.0",
|
|
26
38
|
"react-native-fs": "^2.20.0"
|
|
27
39
|
},
|
package/src/components/Loader.js
CHANGED
|
@@ -5,23 +5,30 @@ import {
|
|
|
5
5
|
Modal,
|
|
6
6
|
Animated,
|
|
7
7
|
Easing,
|
|
8
|
-
Text
|
|
8
|
+
Text,
|
|
9
|
+
Image,
|
|
10
|
+
Dimensions,
|
|
9
11
|
} from "react-native";
|
|
10
|
-
import FastImage from 'react-native-fast-image';
|
|
11
|
-
import { normalize } from "react-native-elements";
|
|
12
12
|
import { getLoaderGif } from "../utils/getLoaderGif";
|
|
13
13
|
|
|
14
|
+
const { width, height } = Dimensions.get("window");
|
|
15
|
+
|
|
16
|
+
// Helper: convert percentage of screen width to px
|
|
17
|
+
const wp = (percent) => (width * percent) / 100;
|
|
18
|
+
|
|
14
19
|
export default function Loader({
|
|
15
20
|
state,
|
|
16
|
-
overlayColor =
|
|
17
|
-
loaderColor =
|
|
18
|
-
size =
|
|
19
|
-
gifSource = {
|
|
20
|
-
|
|
21
|
+
overlayColor = "rgba(0,0,0,0.4)",
|
|
22
|
+
loaderColor = "lightblue",
|
|
23
|
+
size = 12, // % of screen width
|
|
24
|
+
gifSource = {
|
|
25
|
+
uri: "http://emr.amalaims.org:9393/file/getCommonFile/image/Face.gif",
|
|
26
|
+
},
|
|
27
|
+
message = "",
|
|
21
28
|
messageStyle = {},
|
|
22
|
-
animationType =
|
|
29
|
+
animationType = "fade",
|
|
23
30
|
hasBackground = true,
|
|
24
|
-
borderRadius =
|
|
31
|
+
borderRadius = 4, // %
|
|
25
32
|
shadow = true,
|
|
26
33
|
imageurl,
|
|
27
34
|
}) {
|
|
@@ -30,30 +37,29 @@ export default function Loader({
|
|
|
30
37
|
const [fade] = useState(new Animated.Value(0));
|
|
31
38
|
const [imageSource, setImageSource] = useState(gifSource);
|
|
32
39
|
|
|
33
|
-
const error = getLoaderGif(
|
|
40
|
+
const error = getLoaderGif(
|
|
41
|
+
state.animationState,
|
|
42
|
+
state.currentStep,
|
|
43
|
+
"http://emr.amalaims.org:9393/",
|
|
44
|
+
imageurl
|
|
45
|
+
);
|
|
34
46
|
|
|
35
|
-
// Reset imageSource whenever gifSource prop changes
|
|
36
47
|
useEffect(() => {
|
|
37
48
|
setImageSource(gifSource);
|
|
38
49
|
}, [gifSource]);
|
|
39
50
|
|
|
40
51
|
const handleImageError = () => {
|
|
41
|
-
|
|
42
|
-
setImageSource(error);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.error("Loader image error:", err);
|
|
45
|
-
}
|
|
52
|
+
setImageSource(error);
|
|
46
53
|
};
|
|
47
54
|
|
|
48
|
-
// Rotation, pulse, and fade-in animations
|
|
49
55
|
useEffect(() => {
|
|
50
|
-
if (!gifSource) {
|
|
56
|
+
if (!gifSource) {
|
|
51
57
|
Animated.loop(
|
|
52
58
|
Animated.timing(rotation, {
|
|
53
59
|
toValue: 1,
|
|
54
60
|
duration: 1500,
|
|
55
61
|
easing: Easing.linear,
|
|
56
|
-
useNativeDriver: true
|
|
62
|
+
useNativeDriver: true,
|
|
57
63
|
})
|
|
58
64
|
).start();
|
|
59
65
|
}
|
|
@@ -63,80 +69,90 @@ export default function Loader({
|
|
|
63
69
|
Animated.timing(pulse, {
|
|
64
70
|
toValue: 1.1,
|
|
65
71
|
duration: 800,
|
|
66
|
-
useNativeDriver: true
|
|
72
|
+
useNativeDriver: true,
|
|
67
73
|
}),
|
|
68
74
|
Animated.timing(pulse, {
|
|
69
75
|
toValue: 1,
|
|
70
76
|
duration: 800,
|
|
71
|
-
useNativeDriver: true
|
|
72
|
-
})
|
|
77
|
+
useNativeDriver: true,
|
|
78
|
+
}),
|
|
73
79
|
])
|
|
74
80
|
).start();
|
|
75
81
|
|
|
76
82
|
Animated.timing(fade, {
|
|
77
83
|
toValue: 1,
|
|
78
84
|
duration: 300,
|
|
79
|
-
useNativeDriver: true
|
|
85
|
+
useNativeDriver: true,
|
|
80
86
|
}).start();
|
|
81
87
|
}, []);
|
|
82
88
|
|
|
83
89
|
const spin = rotation.interpolate({
|
|
84
90
|
inputRange: [0, 1],
|
|
85
|
-
outputRange: [
|
|
91
|
+
outputRange: ["0deg", "360deg"],
|
|
86
92
|
});
|
|
87
93
|
|
|
94
|
+
const loaderSize = wp(size);
|
|
95
|
+
const borderSize = loaderSize * 0.12;
|
|
96
|
+
|
|
88
97
|
const loaderContent = gifSource ? (
|
|
89
|
-
<
|
|
90
|
-
style={[
|
|
98
|
+
<Image
|
|
99
|
+
style={[
|
|
100
|
+
styles.icon,
|
|
101
|
+
{ width: loaderSize, height: loaderSize },
|
|
102
|
+
]}
|
|
91
103
|
source={imageSource}
|
|
92
|
-
resizeMode={FastImage.resizeMode.contain}
|
|
93
104
|
onError={handleImageError}
|
|
94
105
|
/>
|
|
95
106
|
) : (
|
|
96
|
-
<Animated.View
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
borderColor: loaderColor,
|
|
100
|
-
transform: [{ rotate: spin }, { scale: pulse }],
|
|
101
|
-
width: normalize(size),
|
|
102
|
-
height: normalize(size),
|
|
103
|
-
borderWidth: normalize(size / 10)
|
|
104
|
-
}
|
|
105
|
-
]}>
|
|
106
|
-
<View style={[
|
|
107
|
-
styles.innerCircle,
|
|
107
|
+
<Animated.View
|
|
108
|
+
style={[
|
|
109
|
+
styles.defaultLoader,
|
|
108
110
|
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
width: loaderSize,
|
|
112
|
+
height: loaderSize,
|
|
113
|
+
borderWidth: borderSize,
|
|
114
|
+
borderColor: loaderColor,
|
|
115
|
+
transform: [{ rotate: spin }, { scale: pulse }],
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
<View
|
|
120
|
+
style={[
|
|
121
|
+
styles.innerCircle,
|
|
122
|
+
{
|
|
123
|
+
width: loaderSize / 2,
|
|
124
|
+
height: loaderSize / 2,
|
|
125
|
+
backgroundColor: loaderColor,
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
114
129
|
</Animated.View>
|
|
115
130
|
);
|
|
116
131
|
|
|
117
132
|
return (
|
|
118
133
|
<Modal
|
|
119
134
|
animationType={animationType}
|
|
120
|
-
transparent
|
|
135
|
+
transparent
|
|
121
136
|
visible={state.isLoading}
|
|
122
|
-
onRequestClose={() => {
|
|
137
|
+
onRequestClose={() => {}}
|
|
123
138
|
>
|
|
124
|
-
<Animated.View
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
backgroundColor: overlayColor,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
<Animated.View
|
|
140
|
+
style={[
|
|
141
|
+
styles.modalContainer,
|
|
142
|
+
{ backgroundColor: overlayColor, opacity: fade },
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
<Animated.View
|
|
146
|
+
style={[
|
|
147
|
+
styles.loaderContainer,
|
|
148
|
+
{
|
|
149
|
+
backgroundColor: hasBackground ? "white" : "transparent",
|
|
150
|
+
borderRadius: wp(borderRadius),
|
|
151
|
+
transform: [{ scale: pulse }],
|
|
152
|
+
},
|
|
153
|
+
shadow && styles.shadowStyle,
|
|
154
|
+
]}
|
|
155
|
+
>
|
|
140
156
|
{loaderContent}
|
|
141
157
|
{message ? (
|
|
142
158
|
<Text style={[styles.messageText, messageStyle]}>
|
|
@@ -152,40 +168,36 @@ export default function Loader({
|
|
|
152
168
|
const styles = StyleSheet.create({
|
|
153
169
|
modalContainer: {
|
|
154
170
|
flex: 1,
|
|
155
|
-
justifyContent:
|
|
156
|
-
alignItems:
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
alignItems: "center",
|
|
157
173
|
},
|
|
158
174
|
loaderContainer: {
|
|
159
|
-
padding:
|
|
160
|
-
justifyContent: 'center',
|
|
161
|
-
alignItems: 'center',
|
|
162
|
-
},
|
|
163
|
-
icon_style: {
|
|
175
|
+
padding: wp(5),
|
|
164
176
|
justifyContent: "center",
|
|
165
|
-
alignItems: "center"
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
},
|
|
179
|
+
icon: {
|
|
180
|
+
resizeMode: "contain",
|
|
166
181
|
},
|
|
167
182
|
defaultLoader: {
|
|
168
|
-
borderRadius:
|
|
169
|
-
justifyContent:
|
|
170
|
-
alignItems:
|
|
183
|
+
borderRadius: 1000,
|
|
184
|
+
justifyContent: "center",
|
|
185
|
+
alignItems: "center",
|
|
171
186
|
},
|
|
172
187
|
innerCircle: {
|
|
173
|
-
borderRadius:
|
|
188
|
+
borderRadius: 1000,
|
|
174
189
|
},
|
|
175
190
|
messageText: {
|
|
176
|
-
marginTop:
|
|
177
|
-
fontSize:
|
|
178
|
-
color:
|
|
179
|
-
textAlign:
|
|
191
|
+
marginTop: wp(3),
|
|
192
|
+
fontSize: wp(3.5),
|
|
193
|
+
color: "#555",
|
|
194
|
+
textAlign: "center",
|
|
180
195
|
},
|
|
181
196
|
shadowStyle: {
|
|
182
197
|
shadowColor: "#000",
|
|
183
|
-
shadowOffset: {
|
|
184
|
-
width: 0,
|
|
185
|
-
height: 2,
|
|
186
|
-
},
|
|
198
|
+
shadowOffset: { width: 0, height: 2 },
|
|
187
199
|
shadowOpacity: 0.25,
|
|
188
200
|
shadowRadius: 3.84,
|
|
189
201
|
elevation: 5,
|
|
190
|
-
}
|
|
202
|
+
},
|
|
191
203
|
});
|
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { Global } from '../utils/Global';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Custom hook for a countdown timer with pause/resume functionality.
|
|
6
5
|
*
|
|
6
|
+
* @param {number} duration - Countdown duration in seconds
|
|
7
7
|
* @param {Function} onExpire - Callback fired when countdown reaches zero.
|
|
8
|
-
* @returns {Object}
|
|
8
|
+
* @returns {Object}
|
|
9
9
|
*/
|
|
10
|
-
export const useCountdown = (onExpire) => {
|
|
11
|
-
const [countdown, setCountdown] = useState(
|
|
10
|
+
export const useCountdown = (duration, onExpire) => {
|
|
11
|
+
const [countdown, setCountdown] = useState(duration);
|
|
12
12
|
const timerRef = useRef(null);
|
|
13
|
-
const countdownRef = useRef(
|
|
13
|
+
const countdownRef = useRef(duration);
|
|
14
14
|
const isPausedRef = useRef(false);
|
|
15
15
|
const onExpireRef = useRef(onExpire);
|
|
16
16
|
|
|
17
|
-
//
|
|
17
|
+
// Keep onExpire updated
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
onExpireRef.current = onExpire;
|
|
20
20
|
}, [onExpire]);
|
|
21
21
|
|
|
22
|
+
// Update duration dynamically if it changes
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
countdownRef.current = duration;
|
|
25
|
+
setCountdown(duration);
|
|
26
|
+
}, [duration]);
|
|
27
|
+
|
|
22
28
|
// Start or restart the countdown
|
|
23
29
|
const startCountdown = useCallback((onExpireCallback) => {
|
|
24
30
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
setCountdown(Global.CountdownDuration);
|
|
31
|
+
countdownRef.current = duration;
|
|
32
|
+
setCountdown(duration);
|
|
28
33
|
isPausedRef.current = false;
|
|
29
34
|
|
|
30
|
-
// Clear any existing timer
|
|
31
35
|
if (timerRef.current) {
|
|
32
36
|
clearInterval(timerRef.current);
|
|
33
37
|
timerRef.current = null;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
// Use provided callback or stored one
|
|
37
40
|
const expireCallback = onExpireCallback || onExpireRef.current;
|
|
38
41
|
|
|
39
|
-
// Start new timer
|
|
40
42
|
timerRef.current = setInterval(() => {
|
|
41
43
|
if (isPausedRef.current) return;
|
|
42
44
|
|
|
@@ -45,10 +47,7 @@ export const useCountdown = (onExpire) => {
|
|
|
45
47
|
if (countdownRef.current <= 0) {
|
|
46
48
|
clearInterval(timerRef.current);
|
|
47
49
|
timerRef.current = null;
|
|
48
|
-
|
|
49
|
-
if (typeof expireCallback === 'function') {
|
|
50
|
-
expireCallback();
|
|
51
|
-
}
|
|
50
|
+
expireCallback?.();
|
|
52
51
|
} else {
|
|
53
52
|
setCountdown(countdownRef.current);
|
|
54
53
|
}
|
|
@@ -56,23 +55,20 @@ export const useCountdown = (onExpire) => {
|
|
|
56
55
|
} catch (error) {
|
|
57
56
|
console.error('Error in startCountdown:', error);
|
|
58
57
|
}
|
|
59
|
-
}, []);
|
|
58
|
+
}, [duration]);
|
|
60
59
|
|
|
61
|
-
// Pause the countdown
|
|
62
60
|
const pauseCountdown = useCallback(() => {
|
|
63
61
|
isPausedRef.current = true;
|
|
64
62
|
}, []);
|
|
65
63
|
|
|
66
|
-
// Resume the countdown
|
|
67
64
|
const resumeCountdown = useCallback(() => {
|
|
68
65
|
isPausedRef.current = false;
|
|
69
66
|
}, []);
|
|
70
67
|
|
|
71
|
-
// Reset countdown to initial duration
|
|
72
68
|
const resetCountdown = useCallback(() => {
|
|
73
69
|
try {
|
|
74
|
-
countdownRef.current =
|
|
75
|
-
setCountdown(
|
|
70
|
+
countdownRef.current = duration;
|
|
71
|
+
setCountdown(duration);
|
|
76
72
|
isPausedRef.current = false;
|
|
77
73
|
|
|
78
74
|
if (timerRef.current) {
|
|
@@ -82,15 +78,19 @@ export const useCountdown = (onExpire) => {
|
|
|
82
78
|
} catch (error) {
|
|
83
79
|
console.error('Error in resetCountdown:', error);
|
|
84
80
|
}
|
|
85
|
-
}, []);
|
|
81
|
+
}, [duration]);
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const getCurrentCountdown = useCallback(
|
|
84
|
+
() => countdownRef.current,
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const isPaused = useCallback(
|
|
89
|
+
() => isPausedRef.current,
|
|
90
|
+
[]
|
|
91
|
+
);
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// Cleanup
|
|
94
94
|
useEffect(() => {
|
|
95
95
|
return () => {
|
|
96
96
|
if (timerRef.current) {
|
|
@@ -100,13 +100,13 @@ export const useCountdown = (onExpire) => {
|
|
|
100
100
|
};
|
|
101
101
|
}, []);
|
|
102
102
|
|
|
103
|
-
return {
|
|
104
|
-
countdown,
|
|
105
|
-
startCountdown,
|
|
106
|
-
resetCountdown,
|
|
107
|
-
pauseCountdown,
|
|
103
|
+
return {
|
|
104
|
+
countdown,
|
|
105
|
+
startCountdown,
|
|
106
|
+
resetCountdown,
|
|
107
|
+
pauseCountdown,
|
|
108
108
|
resumeCountdown,
|
|
109
109
|
getCurrentCountdown,
|
|
110
|
-
isPaused
|
|
110
|
+
isPaused,
|
|
111
111
|
};
|
|
112
112
|
};
|
|
@@ -7,29 +7,7 @@ import {
|
|
|
7
7
|
initializeFaceAntiSpoof,
|
|
8
8
|
isFaceAntiSpoofAvailable,
|
|
9
9
|
} from 'react-native-vision-camera-spoof-detector';
|
|
10
|
-
|
|
11
|
-
// Optimized constants - tuned for performance
|
|
12
|
-
const FACE_STABILITY_THRESHOLD = 3;
|
|
13
|
-
const FACE_MOVEMENT_THRESHOLD = 15;
|
|
14
|
-
const FRAME_PROCESSOR_MIN_INTERVAL_MS = 500;
|
|
15
|
-
const MIN_FACE_SIZE = 0.2;
|
|
16
|
-
|
|
17
|
-
// Blink detection
|
|
18
|
-
const BLINK_THRESHOLD = 0.3;
|
|
19
|
-
const REQUIRED_BLINKS = 3;
|
|
20
|
-
|
|
21
|
-
// Anti-spoofing
|
|
22
|
-
const ANTI_SPOOF_CONFIDENCE_THRESHOLD = 0.7;
|
|
23
|
-
const REQUIRED_CONSECUTIVE_LIVE_FRAMES = 3;
|
|
24
|
-
|
|
25
|
-
// Face centering
|
|
26
|
-
const FACE_CENTER_THRESHOLD_X = 0.2;
|
|
27
|
-
const FACE_CENTER_THRESHOLD_Y = 0.15;
|
|
28
|
-
const MIN_FACE_CENTERED_FRAMES = 2;
|
|
29
|
-
|
|
30
|
-
// Performance optimization constants
|
|
31
|
-
const MAX_FRAME_PROCESSING_TIME_MS = 500;
|
|
32
|
-
const BATCH_UPDATE_THRESHOLD = 3;
|
|
10
|
+
import { Global } from 'react-native-biometric-verifier/src/utils/Global';
|
|
33
11
|
|
|
34
12
|
export const useFaceDetectionFrameProcessor = ({
|
|
35
13
|
onStableFaceDetected = () => { },
|
|
@@ -46,7 +24,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
46
24
|
landmarkMode: 'none',
|
|
47
25
|
contourMode: 'none',
|
|
48
26
|
classificationMode: livenessLevel === 1 ? 'all' : 'none',
|
|
49
|
-
minFaceSize: MIN_FACE_SIZE,
|
|
27
|
+
minFaceSize: Global.MIN_FACE_SIZE,
|
|
50
28
|
});
|
|
51
29
|
|
|
52
30
|
const isMounted = useRef(true);
|
|
@@ -230,8 +208,8 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
230
208
|
const frameCenterY = frameHeight / 2;
|
|
231
209
|
|
|
232
210
|
return (
|
|
233
|
-
Math.abs(faceCenterX - frameCenterX) <= frameWidth * FACE_CENTER_THRESHOLD_X &&
|
|
234
|
-
Math.abs(faceCenterY - frameCenterY) <= frameHeight * FACE_CENTER_THRESHOLD_Y
|
|
211
|
+
Math.abs(faceCenterX - frameCenterX) <= frameWidth * Global.FACE_CENTER_THRESHOLD_X &&
|
|
212
|
+
Math.abs(faceCenterY - frameCenterY) <= frameHeight * Global.FACE_CENTER_THRESHOLD_Y
|
|
235
213
|
);
|
|
236
214
|
});
|
|
237
215
|
|
|
@@ -243,7 +221,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
243
221
|
state.flags.captured ||
|
|
244
222
|
isLoading ||
|
|
245
223
|
!state.flags.isActive ||
|
|
246
|
-
(now - state.lastProcessedTime <
|
|
224
|
+
(now - state.lastProcessedTime < Global.FACE_MOVEMENT_THRESHOLD)
|
|
247
225
|
);
|
|
248
226
|
});
|
|
249
227
|
|
|
@@ -265,7 +243,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
265
243
|
}
|
|
266
244
|
|
|
267
245
|
// Performance guard - don't process if taking too long
|
|
268
|
-
if (processingStart - frameProcessingStartTime.current < MAX_FRAME_PROCESSING_TIME_MS) {
|
|
246
|
+
if (processingStart - frameProcessingStartTime.current < Global.MAX_FRAME_PROCESSING_TIME_MS) {
|
|
269
247
|
frame.release?.();
|
|
270
248
|
return;
|
|
271
249
|
}
|
|
@@ -348,13 +326,13 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
348
326
|
|
|
349
327
|
if (centered) {
|
|
350
328
|
state.centering.centeredFrames = Math.min(
|
|
351
|
-
MIN_FACE_CENTERED_FRAMES,
|
|
329
|
+
Global.MIN_FACE_CENTERED_FRAMES,
|
|
352
330
|
state.centering.centeredFrames + 1
|
|
353
331
|
);
|
|
354
332
|
} else {
|
|
355
333
|
state.centering.centeredFrames = 0;
|
|
356
334
|
}
|
|
357
|
-
state.flags.isFaceCentered = state.centering.centeredFrames >= MIN_FACE_CENTERED_FRAMES;
|
|
335
|
+
state.flags.isFaceCentered = state.centering.centeredFrames >= Global.MIN_FACE_CENTERED_FRAMES;
|
|
358
336
|
|
|
359
337
|
// Anti-spoof detection only when face is centered and single
|
|
360
338
|
if (state.flags.isFaceCentered) {
|
|
@@ -366,19 +344,19 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
366
344
|
const isLive = antiSpoofResult.isLive === true;
|
|
367
345
|
const confidence = antiSpoofResult.combinedScore || antiSpoofResult.neuralNetworkScore || 0;
|
|
368
346
|
|
|
369
|
-
if (isLive && confidence > ANTI_SPOOF_CONFIDENCE_THRESHOLD) {
|
|
347
|
+
if (isLive && confidence > Global.ANTI_SPOOF_CONFIDENCE_THRESHOLD) {
|
|
370
348
|
state.antiSpoof.consecutiveLiveFrames = Math.min(
|
|
371
|
-
REQUIRED_CONSECUTIVE_LIVE_FRAMES,
|
|
349
|
+
Global.REQUIRED_CONSECUTIVE_LIVE_FRAMES,
|
|
372
350
|
state.antiSpoof.consecutiveLiveFrames + 1
|
|
373
351
|
);
|
|
374
352
|
} else {
|
|
375
353
|
state.antiSpoof.consecutiveLiveFrames = Math.max(0, state.antiSpoof.consecutiveLiveFrames - 1);
|
|
376
354
|
}
|
|
377
|
-
state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES;
|
|
355
|
+
state.antiSpoof.isLive = state.antiSpoof.consecutiveLiveFrames >= Global.REQUIRED_CONSECUTIVE_LIVE_FRAMES;
|
|
378
356
|
state.antiSpoof.confidence = confidence;
|
|
379
357
|
|
|
380
358
|
// Batch anti-spoof updates
|
|
381
|
-
if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
|
|
359
|
+
if (state.performance.batchCounter % Global.BATCH_UPDATE_THRESHOLD === 0) {
|
|
382
360
|
runOnAntiSpoof({
|
|
383
361
|
isLive: state.antiSpoof.isLive,
|
|
384
362
|
confidence: state.antiSpoof.confidence,
|
|
@@ -410,7 +388,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
410
388
|
else if (newLivenessStep === 1) {
|
|
411
389
|
const leftEye = face.leftEyeOpenProbability ?? 1;
|
|
412
390
|
const rightEye = face.rightEyeOpenProbability ?? 1;
|
|
413
|
-
const eyesClosed = leftEye < BLINK_THRESHOLD && rightEye < BLINK_THRESHOLD;
|
|
391
|
+
const eyesClosed = leftEye < Global.BLINK_THRESHOLD && rightEye < Global.BLINK_THRESHOLD;
|
|
414
392
|
|
|
415
393
|
if (eyesClosed && !newEyeClosed) {
|
|
416
394
|
newBlinkCount++;
|
|
@@ -420,7 +398,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
420
398
|
newEyeClosed = false;
|
|
421
399
|
}
|
|
422
400
|
|
|
423
|
-
if (newBlinkCount >= REQUIRED_BLINKS) {
|
|
401
|
+
if (newBlinkCount >= Global.REQUIRED_BLINKS) {
|
|
424
402
|
newLivenessStep = 2;
|
|
425
403
|
runOnLiveness(newLivenessStep);
|
|
426
404
|
}
|
|
@@ -434,7 +412,7 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
434
412
|
} else {
|
|
435
413
|
const dx = Math.abs(x - state.faceTracking.lastX);
|
|
436
414
|
const dy = Math.abs(y - state.faceTracking.lastY);
|
|
437
|
-
newStableCount = (dx < FACE_MOVEMENT_THRESHOLD && dy < FACE_MOVEMENT_THRESHOLD)
|
|
415
|
+
newStableCount = (dx < Global.FACE_MOVEMENT_THRESHOLD && dy < Global.FACE_MOVEMENT_THRESHOLD)
|
|
438
416
|
? state.faceTracking.stableCount + 1
|
|
439
417
|
: 1;
|
|
440
418
|
}
|
|
@@ -451,10 +429,10 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
451
429
|
state.flags.eyeClosed = newEyeClosed;
|
|
452
430
|
state.performance.batchCounter++;
|
|
453
431
|
|
|
454
|
-
const progress = Math.min(100, (newStableCount / FACE_STABILITY_THRESHOLD) * 100);
|
|
432
|
+
const progress = Math.min(100, (newStableCount / Global.FACE_STABILITY_THRESHOLD) * 100);
|
|
455
433
|
|
|
456
434
|
// Batch face updates
|
|
457
|
-
if (state.performance.batchCounter % BATCH_UPDATE_THRESHOLD === 0) {
|
|
435
|
+
if (state.performance.batchCounter % Global.BATCH_UPDATE_THRESHOLD === 0) {
|
|
458
436
|
runOnFaces(1, progress, newLivenessStep, state.flags.isFaceCentered, {
|
|
459
437
|
isLive: state.antiSpoof.isLive,
|
|
460
438
|
confidence: state.antiSpoof.confidence,
|
|
@@ -466,14 +444,14 @@ export const useFaceDetectionFrameProcessor = ({
|
|
|
466
444
|
|
|
467
445
|
// Capture condition - optimized
|
|
468
446
|
const shouldCapture = !state.flags.captured && (
|
|
469
|
-
newStableCount >= FACE_STABILITY_THRESHOLD &&
|
|
447
|
+
newStableCount >= Global.FACE_STABILITY_THRESHOLD &&
|
|
470
448
|
state.antiSpoof.isLive &&
|
|
471
|
-
state.antiSpoof.consecutiveLiveFrames >= REQUIRED_CONSECUTIVE_LIVE_FRAMES &&
|
|
449
|
+
state.antiSpoof.consecutiveLiveFrames >= Global.REQUIRED_CONSECUTIVE_LIVE_FRAMES &&
|
|
472
450
|
state.flags.isFaceCentered &&
|
|
473
451
|
(localState.livenessLevel === 0 || (
|
|
474
452
|
localState.livenessLevel === 1 &&
|
|
475
453
|
newLivenessStep === 2 &&
|
|
476
|
-
newBlinkCount >= REQUIRED_BLINKS
|
|
454
|
+
newBlinkCount >= Global.REQUIRED_BLINKS
|
|
477
455
|
))
|
|
478
456
|
);
|
|
479
457
|
|
package/src/index.js
CHANGED
|
@@ -16,8 +16,6 @@ import {
|
|
|
16
16
|
Animated,
|
|
17
17
|
} from "react-native";
|
|
18
18
|
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
19
|
-
import { useNavigation } from "@react-navigation/native";
|
|
20
|
-
|
|
21
19
|
// Custom hooks
|
|
22
20
|
import { useCountdown } from "./hooks/useCountdown";
|
|
23
21
|
import { useGeolocation } from "./hooks/useGeolocation";
|
|
@@ -40,11 +38,9 @@ import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
|
40
38
|
import StepIndicator from "./components/StepIndicator";
|
|
41
39
|
|
|
42
40
|
const BiometricModal = React.memo(
|
|
43
|
-
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, fileurl, imageurl }) => {
|
|
44
|
-
const navigation = useNavigation();
|
|
45
|
-
|
|
41
|
+
({ data, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, fileurl, imageurl, navigation, MaxDistanceMeters = 100, duration = 100 }) => {
|
|
46
42
|
// Custom hooks
|
|
47
|
-
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown();
|
|
43
|
+
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
48
44
|
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
49
45
|
const { convertImageToBase64 } = useImageProcessing();
|
|
50
46
|
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
@@ -375,8 +371,29 @@ const BiometricModal = React.memo(
|
|
|
375
371
|
location.longitude
|
|
376
372
|
);
|
|
377
373
|
|
|
378
|
-
if (distance <=
|
|
374
|
+
if (distance <= MaxDistanceMeters) {
|
|
375
|
+
const locationDetails = {
|
|
376
|
+
qrLocation: {
|
|
377
|
+
latitude: lat,
|
|
378
|
+
longitude: lng,
|
|
379
|
+
},
|
|
380
|
+
deviceLocation: {
|
|
381
|
+
latitude: location.latitude,
|
|
382
|
+
longitude: location.longitude,
|
|
383
|
+
accuracy: location.accuracy,
|
|
384
|
+
},
|
|
385
|
+
distanceMeters: distance,
|
|
386
|
+
verified: true,
|
|
387
|
+
verifiedAt: new Date().toISOString(),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
responseRef.current = {
|
|
391
|
+
...(responseRef.current || {}), // existing faceData
|
|
392
|
+
location: locationDetails,
|
|
393
|
+
};
|
|
394
|
+
|
|
379
395
|
safeCallback(responseRef.current);
|
|
396
|
+
|
|
380
397
|
notifyMessage("Location verified successfully!", "success");
|
|
381
398
|
|
|
382
399
|
updateState({
|
|
@@ -392,7 +409,8 @@ const BiometricModal = React.memo(
|
|
|
392
409
|
resetTimeoutRef.current = setTimeout(() => {
|
|
393
410
|
resetState();
|
|
394
411
|
}, 1200);
|
|
395
|
-
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
396
414
|
handleProcessError(
|
|
397
415
|
`Location mismatch (${distance.toFixed(0)}m away).`
|
|
398
416
|
);
|
|
@@ -536,7 +554,7 @@ const BiometricModal = React.memo(
|
|
|
536
554
|
|
|
537
555
|
<View style={styles.timerContainer}>
|
|
538
556
|
<CountdownTimer
|
|
539
|
-
duration={
|
|
557
|
+
duration={duration}
|
|
540
558
|
currentTime={countdown}
|
|
541
559
|
/>
|
|
542
560
|
</View>
|
package/src/utils/Global.js
CHANGED
|
@@ -42,7 +42,27 @@ export class Global {
|
|
|
42
42
|
format: 'JPEG', // 'PNG' or 'JPEG'
|
|
43
43
|
quality: 85, // 0–100
|
|
44
44
|
};
|
|
45
|
+
// Optimized constants - tuned for performance
|
|
46
|
+
static FACE_STABILITY_THRESHOLD = 3;
|
|
47
|
+
static FACE_MOVEMENT_THRESHOLD = 15;
|
|
48
|
+
static FRAME_PROCESSOR_MIN_INTERVAL_MS = 500;
|
|
49
|
+
static MIN_FACE_SIZE = 0.2;
|
|
50
|
+
|
|
51
|
+
// Blink detection
|
|
52
|
+
static BLINK_THRESHOLD = 0.3;
|
|
53
|
+
static REQUIRED_BLINKS = 3;
|
|
54
|
+
|
|
55
|
+
// Anti-spoofing
|
|
56
|
+
static ANTI_SPOOF_CONFIDENCE_THRESHOLD = 0.7;
|
|
57
|
+
static REQUIRED_CONSECUTIVE_LIVE_FRAMES = 3;
|
|
58
|
+
|
|
59
|
+
// Face centering
|
|
60
|
+
static FACE_CENTER_THRESHOLD_X = 0.2;
|
|
61
|
+
static FACE_CENTER_THRESHOLD_Y = 0.15;
|
|
62
|
+
static MIN_FACE_CENTERED_FRAMES = 2;
|
|
63
|
+
|
|
64
|
+
// Performance optimization constants
|
|
65
|
+
static MAX_FRAME_PROCESSING_TIME_MS = 500;
|
|
66
|
+
static BATCH_UPDATE_THRESHOLD = 3;
|
|
45
67
|
|
|
46
|
-
static CountdownDuration = 100; // seconds
|
|
47
|
-
static MaxDistanceMeters = 100; // Max allowed distance for QR verification
|
|
48
68
|
}
|