react-native-biometric-verifier 0.0.42 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/Notification.js +14 -38
- package/src/components/StepIndicator.js +34 -32
- package/src/index.js +193 -151
package/package.json
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { Animated, Text, Platform, StyleSheet } from 'react-native';
|
|
3
3
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
4
|
-
import PropTypes from 'prop-types';
|
|
5
4
|
import { Global } from '../utils/Global';
|
|
6
5
|
|
|
7
|
-
export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
8
|
-
|
|
9
|
-
console.warn('Notification: Invalid or missing notification object');
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
6
|
+
export const Notification = ({ notification = {}, fadeAnim, slideAnim }) => {
|
|
7
|
+
const { visible = false, type = 'info', message = '' } = notification;
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
// Animations (must ALWAYS exist)
|
|
10
|
+
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
11
|
+
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
15
12
|
|
|
16
|
-
// Icon and color mapping
|
|
17
13
|
const iconMap = {
|
|
18
14
|
success: { name: 'check-circle', color: Global.AppTheme.success },
|
|
19
15
|
error: { name: 'error', color: Global.AppTheme.error },
|
|
20
16
|
info: { name: 'info', color: Global.AppTheme.info },
|
|
21
17
|
};
|
|
22
18
|
|
|
23
|
-
const { name: iconName, color: iconColor } =
|
|
24
|
-
|
|
25
|
-
// Heartbeat animation (scale in/out)
|
|
26
|
-
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
27
|
-
|
|
28
|
-
// Shake animation (rotation wiggle)
|
|
29
|
-
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
19
|
+
const { name: iconName, color: iconColor } =
|
|
20
|
+
iconMap[type] || iconMap.info;
|
|
30
21
|
|
|
31
22
|
useEffect(() => {
|
|
32
23
|
const heartbeat = Animated.loop(
|
|
@@ -78,20 +69,20 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
|
78
69
|
heartbeat.stop();
|
|
79
70
|
shake.stop();
|
|
80
71
|
};
|
|
81
|
-
}, [visible, type
|
|
72
|
+
}, [visible, type]);
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
const shakeInterpolate = shakeAnim.interpolate({
|
|
74
|
+
const shakeRotate = shakeAnim.interpolate({
|
|
85
75
|
inputRange: [-1, 1],
|
|
86
76
|
outputRange: ['-10deg', '10deg'],
|
|
87
77
|
});
|
|
88
78
|
|
|
89
79
|
return (
|
|
90
80
|
<Animated.View
|
|
81
|
+
pointerEvents={visible ? 'auto' : 'none'}
|
|
91
82
|
style={[
|
|
92
83
|
styles.container,
|
|
93
84
|
{
|
|
94
|
-
opacity:
|
|
85
|
+
opacity: visible ? 1 : 0,
|
|
95
86
|
transform: [
|
|
96
87
|
{ translateY: slideAnim instanceof Animated.Value ? slideAnim : 0 },
|
|
97
88
|
],
|
|
@@ -102,12 +93,13 @@ export const Notification = ({ notification, fadeAnim, slideAnim }) => {
|
|
|
102
93
|
style={{
|
|
103
94
|
transform: [
|
|
104
95
|
{ scale: scaleAnim },
|
|
105
|
-
...(type === 'error' ? [{ rotate:
|
|
96
|
+
...(type === 'error' ? [{ rotate: shakeRotate }] : []),
|
|
106
97
|
],
|
|
107
98
|
}}
|
|
108
99
|
>
|
|
109
|
-
<Icon name={iconName} size={50} color={iconColor}
|
|
100
|
+
<Icon name={iconName} size={50} color={iconColor} />
|
|
110
101
|
</Animated.View>
|
|
102
|
+
|
|
111
103
|
<Text style={styles.message}>
|
|
112
104
|
{message || 'No message provided'}
|
|
113
105
|
</Text>
|
|
@@ -142,20 +134,4 @@ const styles = StyleSheet.create({
|
|
|
142
134
|
},
|
|
143
135
|
});
|
|
144
136
|
|
|
145
|
-
Notification.propTypes = {
|
|
146
|
-
notification: PropTypes.shape({
|
|
147
|
-
visible: PropTypes.bool.isRequired,
|
|
148
|
-
type: PropTypes.oneOf(['success', 'error', 'info']),
|
|
149
|
-
message: PropTypes.string,
|
|
150
|
-
}),
|
|
151
|
-
fadeAnim: PropTypes.instanceOf(Animated.Value),
|
|
152
|
-
slideAnim: PropTypes.instanceOf(Animated.Value),
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
Notification.defaultProps = {
|
|
156
|
-
notification: { visible: false, type: 'info', message: '' },
|
|
157
|
-
fadeAnim: new Animated.Value(1),
|
|
158
|
-
slideAnim: new Animated.Value(0),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
137
|
export default Notification;
|
|
@@ -7,45 +7,17 @@ import { Global } from '../utils/Global';
|
|
|
7
7
|
const StepIndicator = ({ currentStep, qrscan }) => {
|
|
8
8
|
return (
|
|
9
9
|
<View style={styles.statusContainer}>
|
|
10
|
-
{/* Identity Step */}
|
|
11
|
-
<View style={styles.statusItem}>
|
|
12
|
-
<Icon
|
|
13
|
-
name="face"
|
|
14
|
-
size={20}
|
|
15
|
-
color={
|
|
16
|
-
currentStep === "Identity Verification" ||
|
|
17
|
-
currentStep === "Location Verification" ||
|
|
18
|
-
currentStep === "Complete"
|
|
19
|
-
? Global.AppTheme.primary
|
|
20
|
-
: Global.AppTheme.light
|
|
21
|
-
}
|
|
22
|
-
style={styles.statusIcon}
|
|
23
|
-
/>
|
|
24
|
-
<Text
|
|
25
|
-
style={[
|
|
26
|
-
styles.statusText,
|
|
27
|
-
(currentStep === "Identity Verification" ||
|
|
28
|
-
currentStep === "Location Verification" ||
|
|
29
|
-
currentStep === "Complete") && styles.statusTextActive,
|
|
30
|
-
]}
|
|
31
|
-
>
|
|
32
|
-
Identity
|
|
33
|
-
</Text>
|
|
34
|
-
</View>
|
|
35
|
-
|
|
36
|
-
{/* Show Location only if qrscan = true */}
|
|
37
10
|
{qrscan && (
|
|
38
11
|
<>
|
|
39
|
-
<View style={styles.statusSeparator} />
|
|
40
12
|
<View style={styles.statusItem}>
|
|
41
13
|
<Icon
|
|
42
14
|
name="location-on"
|
|
43
15
|
size={20}
|
|
44
16
|
color={
|
|
45
17
|
currentStep === "Location Verification" ||
|
|
46
|
-
|
|
47
|
-
? Global.AppTheme.
|
|
48
|
-
: Global.AppTheme.
|
|
18
|
+
currentStep === "Complete"
|
|
19
|
+
? Global.AppTheme.light
|
|
20
|
+
: Global.AppTheme.primary
|
|
49
21
|
}
|
|
50
22
|
style={styles.statusIcon}
|
|
51
23
|
/>
|
|
@@ -56,11 +28,41 @@ const StepIndicator = ({ currentStep, qrscan }) => {
|
|
|
56
28
|
currentStep === "Complete") && styles.statusTextActive,
|
|
57
29
|
]}
|
|
58
30
|
>
|
|
59
|
-
|
|
31
|
+
QR
|
|
60
32
|
</Text>
|
|
61
33
|
</View>
|
|
34
|
+
<View style={styles.statusSeparator} />
|
|
62
35
|
</>
|
|
63
36
|
)}
|
|
37
|
+
|
|
38
|
+
{/* Identity Step */}
|
|
39
|
+
<View style={styles.statusItem}>
|
|
40
|
+
<Icon
|
|
41
|
+
name="face"
|
|
42
|
+
size={20}
|
|
43
|
+
color={
|
|
44
|
+
currentStep === "Identity Verification" ||
|
|
45
|
+
currentStep === "Location Verification" ||
|
|
46
|
+
currentStep === "Complete"
|
|
47
|
+
? Global.AppTheme.light
|
|
48
|
+
: Global.AppTheme.primary
|
|
49
|
+
}
|
|
50
|
+
style={styles.statusIcon}
|
|
51
|
+
/>
|
|
52
|
+
<Text
|
|
53
|
+
style={[
|
|
54
|
+
styles.statusText,
|
|
55
|
+
(currentStep === "Identity Verification" ||
|
|
56
|
+
currentStep === "Location Verification" ||
|
|
57
|
+
currentStep === "Complete") && styles.statusTextActive,
|
|
58
|
+
]}
|
|
59
|
+
>
|
|
60
|
+
ID
|
|
61
|
+
</Text>
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
{/* Show Location only if qrscan = true */}
|
|
65
|
+
|
|
64
66
|
</View>
|
|
65
67
|
);
|
|
66
68
|
};
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import React, {
|
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
5
|
useCallback,
|
|
6
|
-
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
forwardRef,
|
|
7
8
|
} from "react";
|
|
8
9
|
import {
|
|
9
10
|
View,
|
|
@@ -37,8 +38,21 @@ import { Notification } from "./components/Notification";
|
|
|
37
38
|
import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
38
39
|
import StepIndicator from "./components/StepIndicator";
|
|
39
40
|
|
|
40
|
-
const BiometricModal =
|
|
41
|
-
|
|
41
|
+
const BiometricModal = forwardRef(({
|
|
42
|
+
data,
|
|
43
|
+
depkey,
|
|
44
|
+
qrscan = false,
|
|
45
|
+
callback,
|
|
46
|
+
apiurl,
|
|
47
|
+
onclose,
|
|
48
|
+
frameProcessorFps,
|
|
49
|
+
livenessLevel,
|
|
50
|
+
fileurl,
|
|
51
|
+
imageurl,
|
|
52
|
+
navigation,
|
|
53
|
+
MaxDistanceMeters = 50,
|
|
54
|
+
duration = 100
|
|
55
|
+
}, ref) => {
|
|
42
56
|
// Custom hooks
|
|
43
57
|
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
44
58
|
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
@@ -48,13 +62,14 @@ const BiometricModal = React.memo(
|
|
|
48
62
|
|
|
49
63
|
// State
|
|
50
64
|
const [modalVisible, setModalVisible] = useState(false);
|
|
51
|
-
const [cameraType, setCameraType] = useState("
|
|
65
|
+
const [cameraType, setCameraType] = useState("back"); // Start with back camera for QR scan
|
|
52
66
|
const [state, setState] = useState({
|
|
53
67
|
isLoading: false,
|
|
54
68
|
loadingType: Global.LoadingTypes.none,
|
|
55
69
|
currentStep: "Start",
|
|
56
70
|
employeeData: null,
|
|
57
|
-
animationState: Global.AnimationStates.
|
|
71
|
+
animationState: Global.AnimationStates.qrScan, // Start with QR scan animation
|
|
72
|
+
qrData: null, // Store QR data for later use in face recognition
|
|
58
73
|
});
|
|
59
74
|
|
|
60
75
|
// Refs
|
|
@@ -63,11 +78,18 @@ const BiometricModal = React.memo(
|
|
|
63
78
|
const responseRef = useRef(null);
|
|
64
79
|
const processedRef = useRef(false);
|
|
65
80
|
const resetTimeoutRef = useRef(null);
|
|
66
|
-
|
|
67
81
|
// Animation values
|
|
68
82
|
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
69
83
|
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
70
84
|
|
|
85
|
+
// Expose methods to parent via ref
|
|
86
|
+
useImperativeHandle(ref, () => ({
|
|
87
|
+
reset: resetState,
|
|
88
|
+
start: startProcess,
|
|
89
|
+
close: resetState,
|
|
90
|
+
getStatus: () => state,
|
|
91
|
+
}));
|
|
92
|
+
|
|
71
93
|
// Cleanup on unmount
|
|
72
94
|
useEffect(() => {
|
|
73
95
|
return () => {
|
|
@@ -145,14 +167,17 @@ const BiometricModal = React.memo(
|
|
|
145
167
|
|
|
146
168
|
// Reset state helper
|
|
147
169
|
const resetState = useCallback(() => {
|
|
148
|
-
onclose
|
|
170
|
+
if (onclose) {
|
|
171
|
+
onclose(false);
|
|
172
|
+
}
|
|
149
173
|
|
|
150
174
|
setState({
|
|
151
175
|
isLoading: false,
|
|
152
176
|
loadingType: Global.LoadingTypes.none,
|
|
153
177
|
currentStep: "Start",
|
|
154
178
|
employeeData: null,
|
|
155
|
-
animationState: Global.AnimationStates.
|
|
179
|
+
animationState: Global.AnimationStates.qrScan,
|
|
180
|
+
qrData: null,
|
|
156
181
|
});
|
|
157
182
|
|
|
158
183
|
setModalVisible(false);
|
|
@@ -164,7 +189,7 @@ const BiometricModal = React.memo(
|
|
|
164
189
|
clearTimeout(resetTimeoutRef.current);
|
|
165
190
|
resetTimeoutRef.current = null;
|
|
166
191
|
}
|
|
167
|
-
}, [resetCountdown, clearNotification]);
|
|
192
|
+
}, [resetCountdown, clearNotification, onclose]);
|
|
168
193
|
|
|
169
194
|
// Error handler
|
|
170
195
|
const handleProcessError = useCallback(
|
|
@@ -195,7 +220,7 @@ const BiometricModal = React.memo(
|
|
|
195
220
|
const handleCountdownFinish = useCallback(() => {
|
|
196
221
|
handleProcessError("Time is up! Please try again.");
|
|
197
222
|
|
|
198
|
-
if (navigation
|
|
223
|
+
if (navigation?.canGoBack?.()) {
|
|
199
224
|
navigation.goBack();
|
|
200
225
|
}
|
|
201
226
|
}, [handleProcessError, navigation]);
|
|
@@ -210,112 +235,7 @@ const BiometricModal = React.memo(
|
|
|
210
235
|
return true;
|
|
211
236
|
}, [apiurl, handleProcessError]);
|
|
212
237
|
|
|
213
|
-
//
|
|
214
|
-
const uploadFaceScan = useCallback(
|
|
215
|
-
async (selfie) => {
|
|
216
|
-
if (!validateApiUrl()) return;
|
|
217
|
-
const currentData = dataRef.current;
|
|
218
|
-
|
|
219
|
-
if (!currentData) {
|
|
220
|
-
handleProcessError("Employee data not found.");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
updateState({
|
|
225
|
-
isLoading: true,
|
|
226
|
-
loadingType: Global.LoadingTypes.faceRecognition,
|
|
227
|
-
animationState: Global.AnimationStates.processing,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
InteractionManager.runAfterInteractions(async () => {
|
|
231
|
-
let base64;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
updateState({
|
|
235
|
-
loadingType: Global.LoadingTypes.imageProcessing,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
base64 = await convertImageToBase64(selfie?.uri);
|
|
239
|
-
} catch (err) {
|
|
240
|
-
console.error("Image conversion failed:", err);
|
|
241
|
-
handleProcessError("Image conversion failed.", err);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!base64) {
|
|
246
|
-
handleProcessError("Failed to process image.");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const body = { image: base64 };
|
|
252
|
-
const header = { faceid: currentData };
|
|
253
|
-
const buttonapi = `${apiurl}python/recognize`;
|
|
254
|
-
|
|
255
|
-
updateState({
|
|
256
|
-
loadingType: Global.LoadingTypes.networkRequest,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const response = await networkServiceCall(
|
|
260
|
-
"POST",
|
|
261
|
-
buttonapi,
|
|
262
|
-
header,
|
|
263
|
-
body
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
if (response?.httpstatus === 200) {
|
|
267
|
-
responseRef.current = response;
|
|
268
|
-
|
|
269
|
-
updateState({
|
|
270
|
-
employeeData: response.data?.data || null,
|
|
271
|
-
animationState: Global.AnimationStates.success,
|
|
272
|
-
isLoading: false,
|
|
273
|
-
loadingType: Global.LoadingTypes.none,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
notifyMessage("Identity verified successfully!", "success");
|
|
277
|
-
|
|
278
|
-
if (qrscan) {
|
|
279
|
-
setTimeout(() => startQRCodeScan(), 1200);
|
|
280
|
-
} else {
|
|
281
|
-
safeCallback(responseRef.current);
|
|
282
|
-
|
|
283
|
-
if (resetTimeoutRef.current) {
|
|
284
|
-
clearTimeout(resetTimeoutRef.current);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
resetTimeoutRef.current = setTimeout(() => {
|
|
288
|
-
resetState();
|
|
289
|
-
}, 1200);
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
handleProcessError(
|
|
293
|
-
response?.data?.message ||
|
|
294
|
-
"Face not recognized. Please try again."
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
} catch (error) {
|
|
298
|
-
console.error("Network request failed:", error);
|
|
299
|
-
handleProcessError(
|
|
300
|
-
"Connection error. Please check your network.",
|
|
301
|
-
error
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
[
|
|
307
|
-
convertImageToBase64,
|
|
308
|
-
notifyMessage,
|
|
309
|
-
qrscan,
|
|
310
|
-
resetState,
|
|
311
|
-
updateState,
|
|
312
|
-
validateApiUrl,
|
|
313
|
-
safeCallback,
|
|
314
|
-
handleProcessError
|
|
315
|
-
]
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
// QR code processing
|
|
238
|
+
// QR code processing - FIRST STEP
|
|
319
239
|
const handleQRScanned = useCallback(
|
|
320
240
|
async (qrCodeData) => {
|
|
321
241
|
if (!validateApiUrl()) return;
|
|
@@ -352,14 +272,12 @@ const BiometricModal = React.memo(
|
|
|
352
272
|
|
|
353
273
|
const location = await getCurrentLocation();
|
|
354
274
|
|
|
355
|
-
const [latStr, lngStr] = qrString.split(",");
|
|
275
|
+
const [latStr, lngStr, qrkey] = qrString.split(",");
|
|
356
276
|
const lat = parseFloat(latStr);
|
|
357
277
|
const lng = parseFloat(lngStr);
|
|
358
278
|
const validCoords = !isNaN(lat) && !isNaN(lng);
|
|
359
|
-
const validDev =
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (validCoords && validDev) {
|
|
279
|
+
const validDev = !isNaN(location?.latitude) && !isNaN(location?.longitude);
|
|
280
|
+
if (validCoords && validDev && qrkey === depkey) {
|
|
363
281
|
updateState({
|
|
364
282
|
loadingType: Global.LoadingTypes.calculateDistance,
|
|
365
283
|
});
|
|
@@ -370,7 +288,6 @@ const BiometricModal = React.memo(
|
|
|
370
288
|
location.latitude,
|
|
371
289
|
location.longitude
|
|
372
290
|
);
|
|
373
|
-
|
|
374
291
|
if (distance <= MaxDistanceMeters) {
|
|
375
292
|
const locationDetails = {
|
|
376
293
|
qrLocation: {
|
|
@@ -387,27 +304,24 @@ const BiometricModal = React.memo(
|
|
|
387
304
|
verifiedAt: new Date().toISOString(),
|
|
388
305
|
};
|
|
389
306
|
|
|
307
|
+
// Store location details and QR data for face recognition
|
|
390
308
|
responseRef.current = {
|
|
391
|
-
...(responseRef.current || {}), // existing faceData
|
|
392
309
|
location: locationDetails,
|
|
393
310
|
};
|
|
394
311
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
notifyMessage("Location verified successfully!", "success");
|
|
398
|
-
|
|
312
|
+
// Store QR data in state for face recognition step
|
|
399
313
|
updateState({
|
|
314
|
+
qrData: qrString,
|
|
400
315
|
animationState: Global.AnimationStates.success,
|
|
401
316
|
isLoading: false,
|
|
402
317
|
loadingType: Global.LoadingTypes.none,
|
|
403
318
|
});
|
|
404
319
|
|
|
405
|
-
|
|
406
|
-
clearTimeout(resetTimeoutRef.current);
|
|
407
|
-
}
|
|
320
|
+
notifyMessage("Location verified successfully!", "success");
|
|
408
321
|
|
|
409
|
-
|
|
410
|
-
|
|
322
|
+
// Start face recognition after a brief delay
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
startFaceRecognition();
|
|
411
325
|
}, 1200);
|
|
412
326
|
}
|
|
413
327
|
else {
|
|
@@ -430,49 +344,168 @@ const BiometricModal = React.memo(
|
|
|
430
344
|
getCurrentLocation,
|
|
431
345
|
notifyMessage,
|
|
432
346
|
requestLocationPermission,
|
|
347
|
+
updateState,
|
|
348
|
+
validateApiUrl,
|
|
349
|
+
handleProcessError,
|
|
350
|
+
depkey,
|
|
351
|
+
MaxDistanceMeters
|
|
352
|
+
]
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Face scan upload - SECOND STEP
|
|
356
|
+
const uploadFaceScan = useCallback(
|
|
357
|
+
async (selfie) => {
|
|
358
|
+
if (!validateApiUrl()) return;
|
|
359
|
+
|
|
360
|
+
// Check if QR scan was completed successfully
|
|
361
|
+
if (!state.qrData) {
|
|
362
|
+
handleProcessError("Please complete QR scan first.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const currentData = dataRef.current;
|
|
367
|
+
|
|
368
|
+
if (!currentData) {
|
|
369
|
+
handleProcessError("Employee data not found.");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
updateState({
|
|
374
|
+
isLoading: true,
|
|
375
|
+
loadingType: Global.LoadingTypes.faceRecognition,
|
|
376
|
+
animationState: Global.AnimationStates.processing,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
380
|
+
let base64;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
updateState({
|
|
384
|
+
loadingType: Global.LoadingTypes.imageProcessing,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
base64 = await convertImageToBase64(selfie?.uri);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error("Image conversion failed:", err);
|
|
390
|
+
handleProcessError("Image conversion failed.", err);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!base64) {
|
|
395
|
+
handleProcessError("Failed to process image.");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const body = { image: base64 };
|
|
401
|
+
const header = { faceid: currentData };
|
|
402
|
+
const buttonapi = `${apiurl}python/recognize`;
|
|
403
|
+
|
|
404
|
+
updateState({
|
|
405
|
+
loadingType: Global.LoadingTypes.networkRequest,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const response = await networkServiceCall(
|
|
409
|
+
"POST",
|
|
410
|
+
buttonapi,
|
|
411
|
+
header,
|
|
412
|
+
body
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (response?.httpstatus === 200) {
|
|
416
|
+
// Combine face recognition response with QR location data
|
|
417
|
+
responseRef.current = {
|
|
418
|
+
...responseRef.current, // Contains location data from QR scan
|
|
419
|
+
...response.data?.data || {},
|
|
420
|
+
faceRecognition: response.data?.data || null,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
updateState({
|
|
424
|
+
employeeData: response.data?.data || null,
|
|
425
|
+
animationState: Global.AnimationStates.success,
|
|
426
|
+
isLoading: false,
|
|
427
|
+
loadingType: Global.LoadingTypes.none,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
notifyMessage("Identity verified successfully!", "success");
|
|
431
|
+
|
|
432
|
+
// Call the callback with combined data
|
|
433
|
+
safeCallback(responseRef.current);
|
|
434
|
+
|
|
435
|
+
if (resetTimeoutRef.current) {
|
|
436
|
+
clearTimeout(resetTimeoutRef.current);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
resetTimeoutRef.current = setTimeout(() => {
|
|
440
|
+
resetState();
|
|
441
|
+
}, 1200);
|
|
442
|
+
} else {
|
|
443
|
+
handleProcessError(
|
|
444
|
+
response?.data?.message ||
|
|
445
|
+
"Face not recognized. Please try again."
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error("Network request failed:", error);
|
|
450
|
+
handleProcessError(
|
|
451
|
+
"Connection error. Please check your network.",
|
|
452
|
+
error
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
[
|
|
458
|
+
convertImageToBase64,
|
|
459
|
+
notifyMessage,
|
|
433
460
|
resetState,
|
|
434
461
|
updateState,
|
|
435
462
|
validateApiUrl,
|
|
436
463
|
safeCallback,
|
|
437
|
-
handleProcessError
|
|
464
|
+
handleProcessError,
|
|
465
|
+
state.qrData,
|
|
466
|
+
apiurl
|
|
438
467
|
]
|
|
439
468
|
);
|
|
440
469
|
|
|
441
470
|
// Image capture handler
|
|
442
471
|
const handleImageCapture = useCallback(
|
|
443
472
|
async (capturedData) => {
|
|
444
|
-
if (state.currentStep === "
|
|
445
|
-
|
|
446
|
-
} else if (state.currentStep === "
|
|
447
|
-
|
|
473
|
+
if (state.currentStep === "Location Verification") {
|
|
474
|
+
await handleQRScanned(capturedData);
|
|
475
|
+
} else if (state.currentStep === "Identity Verification") {
|
|
476
|
+
await uploadFaceScan(capturedData);
|
|
448
477
|
}
|
|
449
478
|
},
|
|
450
479
|
[state.currentStep, uploadFaceScan, handleQRScanned]
|
|
451
480
|
);
|
|
452
481
|
|
|
453
|
-
// Start
|
|
454
|
-
const
|
|
482
|
+
// Start QR code scan - FIRST STEP
|
|
483
|
+
const handleStartQRScan = useCallback(() => {
|
|
455
484
|
updateState({
|
|
456
|
-
currentStep: "
|
|
457
|
-
animationState: Global.AnimationStates.
|
|
485
|
+
currentStep: "Location Verification",
|
|
486
|
+
animationState: Global.AnimationStates.qrScan,
|
|
458
487
|
});
|
|
459
|
-
setCameraType("
|
|
488
|
+
setCameraType("back");
|
|
460
489
|
}, [updateState]);
|
|
461
490
|
|
|
462
|
-
// Start
|
|
463
|
-
const
|
|
491
|
+
// Start face recognition - SECOND STEP
|
|
492
|
+
const startFaceRecognition = useCallback(() => {
|
|
464
493
|
updateState({
|
|
465
|
-
currentStep: "
|
|
466
|
-
animationState: Global.AnimationStates.
|
|
494
|
+
currentStep: "Identity Verification",
|
|
495
|
+
animationState: Global.AnimationStates.faceScan,
|
|
467
496
|
});
|
|
468
|
-
setCameraType("
|
|
497
|
+
setCameraType("front");
|
|
469
498
|
}, [updateState]);
|
|
470
499
|
|
|
471
500
|
// Start the verification process
|
|
472
501
|
const startProcess = useCallback(() => {
|
|
473
502
|
startCountdown(handleCountdownFinish);
|
|
474
|
-
|
|
475
|
-
|
|
503
|
+
if (qrscan) {
|
|
504
|
+
handleStartQRScan();
|
|
505
|
+
} else {
|
|
506
|
+
startFaceRecognition();
|
|
507
|
+
}
|
|
508
|
+
}, [handleCountdownFinish, handleStartQRScan, startCountdown, startFaceRecognition, qrscan]);
|
|
476
509
|
|
|
477
510
|
// Open modal when data is received
|
|
478
511
|
useEffect(() => {
|
|
@@ -535,7 +568,10 @@ const BiometricModal = React.memo(
|
|
|
535
568
|
</View>
|
|
536
569
|
|
|
537
570
|
<View style={styles.topContainerstep}>
|
|
538
|
-
<StepIndicator
|
|
571
|
+
<StepIndicator
|
|
572
|
+
currentStep={state.currentStep}
|
|
573
|
+
qrscan={qrscan}
|
|
574
|
+
/>
|
|
539
575
|
</View>
|
|
540
576
|
|
|
541
577
|
{state.employeeData && (
|
|
@@ -568,6 +604,12 @@ const BiometricModal = React.memo(
|
|
|
568
604
|
}
|
|
569
605
|
);
|
|
570
606
|
|
|
607
|
+
// Wrap with memo after forwardRef
|
|
608
|
+
const MemoizedBiometricModal = React.memo(BiometricModal);
|
|
609
|
+
|
|
610
|
+
// Add display name for debugging
|
|
611
|
+
MemoizedBiometricModal.displayName = 'BiometricModal';
|
|
612
|
+
|
|
571
613
|
const styles = StyleSheet.create({
|
|
572
614
|
modalContainer: {
|
|
573
615
|
flex: 1,
|
|
@@ -662,4 +704,4 @@ const styles = StyleSheet.create({
|
|
|
662
704
|
},
|
|
663
705
|
});
|
|
664
706
|
|
|
665
|
-
export default
|
|
707
|
+
export default MemoizedBiometricModal;
|