react-native-biometric-verifier 0.0.7 → 0.0.9
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/hooks/useSafeCallback.js +24 -0
- package/src/index.js +344 -299
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to safely execute a callback function with error handling
|
|
5
|
+
*/
|
|
6
|
+
export const useSafeCallback = (callback, notifyMessage) => {
|
|
7
|
+
return useCallback(
|
|
8
|
+
(response) => {
|
|
9
|
+
if (typeof callback === "function") {
|
|
10
|
+
try {
|
|
11
|
+
callback(response);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error("Callback execution failed:", err);
|
|
14
|
+
if (typeof notifyMessage === "function") {
|
|
15
|
+
notifyMessage("Unexpected error while processing callback.", "error");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
console.log("Biometric Verification Response:", response);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
[callback, notifyMessage]
|
|
23
|
+
);
|
|
24
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,331 +1,376 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
2
8
|
import {
|
|
3
9
|
View,
|
|
4
10
|
TouchableOpacity,
|
|
5
11
|
Text,
|
|
6
12
|
Modal,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
InteractionManager,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
16
|
+
import { useCountdown } from "./hooks/useCountdown";
|
|
17
|
+
import { useGeolocation } from "./hooks/useGeolocation";
|
|
18
|
+
import { useImageProcessing } from "./hooks/useImageProcessing";
|
|
19
|
+
import { useNotifyMessage } from "./hooks/useNotifyMessage";
|
|
20
|
+
import { getDistanceInMeters } from "./utils/distanceCalculator";
|
|
14
21
|
import {
|
|
15
22
|
ANIMATION_STATES,
|
|
16
23
|
COLORS,
|
|
17
24
|
COUNTDOWN_DURATION,
|
|
18
25
|
MAX_DISTANCE_METERS,
|
|
19
|
-
} from
|
|
20
|
-
import Loader from
|
|
21
|
-
import { CountdownTimer } from
|
|
22
|
-
import { EmployeeCard } from
|
|
23
|
-
import { Notification } from
|
|
24
|
-
import { StateIndicator } from
|
|
25
|
-
import { styles } from
|
|
26
|
-
import { useNavigation } from
|
|
27
|
-
import networkServiceCall from
|
|
28
|
-
import { getLoaderGif } from
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const dataRef = useRef(data);
|
|
47
|
-
const mountedRef = useRef(true);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
return () => {
|
|
51
|
-
mountedRef.current = false;
|
|
52
|
-
};
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
dataRef.current = data;
|
|
57
|
-
}, [data]);
|
|
58
|
-
|
|
59
|
-
const updateState = useCallback((newState) => {
|
|
60
|
-
if (mountedRef.current) {
|
|
61
|
-
setState(prev => ({ ...prev, ...newState }));
|
|
62
|
-
}
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
const resetState = useCallback(() => {
|
|
66
|
-
updateState({
|
|
67
|
-
currentStep: 'Start',
|
|
26
|
+
} from "./utils/constants";
|
|
27
|
+
import Loader from "./components/Loader";
|
|
28
|
+
import { CountdownTimer } from "./components/CountdownTimer";
|
|
29
|
+
import { EmployeeCard } from "./components/EmployeeCard";
|
|
30
|
+
import { Notification } from "./components/Notification";
|
|
31
|
+
import { StateIndicator } from "./components/StateIndicator";
|
|
32
|
+
import { styles } from "./components/styles";
|
|
33
|
+
import { useNavigation } from "@react-navigation/native";
|
|
34
|
+
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
35
|
+
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
36
|
+
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
37
|
+
|
|
38
|
+
const BiometricVerificationModal = React.memo(
|
|
39
|
+
({ data, qrscan = false, callback, apiurl }) => {
|
|
40
|
+
const navigation = useNavigation();
|
|
41
|
+
|
|
42
|
+
const { countdown, startCountdown, resetCountdown } = useCountdown();
|
|
43
|
+
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
44
|
+
const { convertImageToBase64 } = useImageProcessing();
|
|
45
|
+
const { notification, fadeAnim, slideAnim, notifyMessage } =
|
|
46
|
+
useNotifyMessage();
|
|
47
|
+
|
|
48
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
49
|
+
const [state, setState] = useState({
|
|
50
|
+
isLoading: false,
|
|
51
|
+
currentStep: "Start",
|
|
68
52
|
employeeData: null,
|
|
69
53
|
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
70
|
-
isLoading: false,
|
|
71
|
-
});
|
|
72
|
-
resetCountdown();
|
|
73
|
-
}, [resetCountdown, updateState]);
|
|
74
|
-
|
|
75
|
-
const handleCountdownFinish = useCallback(() => {
|
|
76
|
-
notifyMessage('Time is up! Please try again.', 'error');
|
|
77
|
-
updateState({
|
|
78
|
-
modalVisible: false,
|
|
79
|
-
animationState: ANIMATION_STATES.ERROR,
|
|
80
54
|
});
|
|
81
|
-
resetState();
|
|
82
|
-
if (navigation.canGoBack()) navigation.goBack();
|
|
83
|
-
}, [navigation, notifyMessage, resetState, updateState]);
|
|
84
|
-
|
|
85
|
-
const validateApiUrl = useCallback(() => {
|
|
86
|
-
if (!apiurl || typeof apiurl !== 'string') {
|
|
87
|
-
notifyMessage('Invalid API URL configuration.', 'error');
|
|
88
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
}, [apiurl, notifyMessage, updateState]);
|
|
93
|
-
|
|
94
|
-
const uploadFaceScan = useCallback(async (selfie) => {
|
|
95
|
-
if (!validateApiUrl()) return;
|
|
96
|
-
|
|
97
|
-
const currentData = dataRef.current;
|
|
98
|
-
if (!currentData) {
|
|
99
|
-
notifyMessage('Employee data not found.', 'error');
|
|
100
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let base64;
|
|
105
|
-
try {
|
|
106
|
-
base64 = await convertImageToBase64(selfie?.uri);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
notifyMessage('Image conversion failed.', 'error');
|
|
109
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!base64) {
|
|
114
|
-
notifyMessage('Failed to process image.', 'error');
|
|
115
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
updateState({
|
|
120
|
-
isLoading: true,
|
|
121
|
-
animationState: ANIMATION_STATES.PROCESSING,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const body = { image: base64 };
|
|
126
|
-
const header = { faceid: currentData };
|
|
127
|
-
const buttonapi = `${apiurl}python/recognize`;
|
|
128
|
-
|
|
129
|
-
console.log("buttonapi--------------------", buttonapi);
|
|
130
55
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
56
|
+
const dataRef = useRef(data);
|
|
57
|
+
const mountedRef = useRef(true);
|
|
58
|
+
const responseRef = useRef(null);
|
|
59
|
+
const processedRef = useRef(false);
|
|
60
|
+
const lastDataRef = useRef(null);
|
|
61
|
+
|
|
62
|
+
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
63
|
+
|
|
64
|
+
/** Cleanup on unmount */
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
mountedRef.current = false;
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
dataRef.current = data;
|
|
73
|
+
}, [data]);
|
|
74
|
+
|
|
75
|
+
const updateState = useCallback((newState) => {
|
|
76
|
+
if (mountedRef.current) {
|
|
77
|
+
setState((prev) => {
|
|
78
|
+
const merged = { ...prev, ...newState };
|
|
79
|
+
return prev !== merged ? merged : prev;
|
|
138
80
|
});
|
|
139
|
-
|
|
140
|
-
if (qrscan) {
|
|
141
|
-
setTimeout(() => startQRCodeScan(), 1500);
|
|
142
|
-
} else {
|
|
143
|
-
setTimeout(() => {
|
|
144
|
-
try {
|
|
145
|
-
callback?.(dataRef.current);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error("Callback execution failed:", err);
|
|
148
|
-
notifyMessage('Unexpected error after verification.', 'error');
|
|
149
|
-
}
|
|
150
|
-
updateState({ modalVisible: false });
|
|
151
|
-
resetState();
|
|
152
|
-
}, 1500);
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
notifyMessage(
|
|
156
|
-
response?.data?.error?.message || 'Face not recognized. Please try again.',
|
|
157
|
-
'error'
|
|
158
|
-
);
|
|
159
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
160
81
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
hidebuttons: true,
|
|
189
|
-
cameratype: 'back',
|
|
190
|
-
cameramoduletype: 2,
|
|
191
|
-
onSelect: handleQRScanned,
|
|
192
|
-
});
|
|
193
|
-
}, [navigation, updateState]);
|
|
194
|
-
|
|
195
|
-
const handleQRScanned = useCallback(async (qrCodeData) => {
|
|
196
|
-
if (!validateApiUrl()) return;
|
|
197
|
-
|
|
198
|
-
updateState({
|
|
199
|
-
animationState: ANIMATION_STATES.PROCESSING,
|
|
200
|
-
isLoading: true,
|
|
201
|
-
});
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const resetState = useCallback(() => {
|
|
85
|
+
console.log("🔄 Resetting biometric modal...");
|
|
86
|
+
setState({
|
|
87
|
+
isLoading: false,
|
|
88
|
+
currentStep: "Start",
|
|
89
|
+
employeeData: null,
|
|
90
|
+
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
91
|
+
});
|
|
92
|
+
setModalVisible(false);
|
|
93
|
+
processedRef.current = false;
|
|
94
|
+
resetCountdown();
|
|
95
|
+
}, [resetCountdown]);
|
|
96
|
+
|
|
97
|
+
const handleProcessError = useCallback(
|
|
98
|
+
(message, errorObj = null) => {
|
|
99
|
+
if (errorObj) console.error(message, errorObj);
|
|
100
|
+
notifyMessage(message, "error");
|
|
101
|
+
updateState({
|
|
102
|
+
animationState: ANIMATION_STATES.ERROR,
|
|
103
|
+
isLoading: false,
|
|
104
|
+
});
|
|
105
|
+
setTimeout(resetState, 1200);
|
|
106
|
+
},
|
|
107
|
+
[notifyMessage, resetState, updateState]
|
|
108
|
+
);
|
|
202
109
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
110
|
+
const handleCountdownFinish = useCallback(() => {
|
|
111
|
+
handleProcessError("Time is up! Please try again.");
|
|
112
|
+
resetState();
|
|
113
|
+
if (navigation.canGoBack()) navigation.goBack();
|
|
114
|
+
}, [handleProcessError, navigation, resetState]);
|
|
210
115
|
|
|
211
|
-
|
|
212
|
-
if (!
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return;
|
|
116
|
+
const validateApiUrl = useCallback(() => {
|
|
117
|
+
if (!apiurl || typeof apiurl !== "string") {
|
|
118
|
+
handleProcessError("Invalid API URL configuration.");
|
|
119
|
+
return false;
|
|
216
120
|
}
|
|
121
|
+
return true;
|
|
122
|
+
}, [apiurl, handleProcessError]);
|
|
123
|
+
|
|
124
|
+
const uploadFaceScan = useCallback(
|
|
125
|
+
async (selfie) => {
|
|
126
|
+
if (!validateApiUrl()) return;
|
|
127
|
+
const currentData = dataRef.current;
|
|
128
|
+
if (!currentData) {
|
|
129
|
+
handleProcessError("Employee data not found.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
217
132
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
133
|
+
updateState({
|
|
134
|
+
isLoading: true,
|
|
135
|
+
animationState: ANIMATION_STATES.PROCESSING,
|
|
136
|
+
});
|
|
222
137
|
|
|
223
|
-
|
|
224
|
-
|
|
138
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
139
|
+
let base64;
|
|
140
|
+
try {
|
|
141
|
+
base64 = await convertImageToBase64(selfie?.uri);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
handleProcessError("Image conversion failed.", err);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
225
146
|
|
|
226
|
-
|
|
227
|
-
|
|
147
|
+
if (!base64) {
|
|
148
|
+
handleProcessError("Failed to process image.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
228
151
|
|
|
229
|
-
if (distance <= MAX_DISTANCE_METERS) {
|
|
230
152
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
153
|
+
const body = { image: base64 };
|
|
154
|
+
const header = { faceid: currentData };
|
|
155
|
+
const buttonapi = `${apiurl}python/recognize`;
|
|
156
|
+
console.log("buttonapi", buttonapi);
|
|
157
|
+
const response = await networkServiceCall(
|
|
158
|
+
"POST",
|
|
159
|
+
buttonapi,
|
|
160
|
+
header,
|
|
161
|
+
body
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (response?.httpstatus === 200) {
|
|
165
|
+
responseRef.current = response;
|
|
166
|
+
updateState({
|
|
167
|
+
employeeData: response.data?.data || null,
|
|
168
|
+
animationState: ANIMATION_STATES.SUCCESS,
|
|
169
|
+
isLoading: false,
|
|
170
|
+
});
|
|
171
|
+
notifyMessage("Identity verified successfully!", "success");
|
|
172
|
+
|
|
173
|
+
if (qrscan) {
|
|
174
|
+
setTimeout(() => startQRCodeScan(), 1200);
|
|
175
|
+
} else {
|
|
176
|
+
safeCallback(responseRef.current);
|
|
177
|
+
setTimeout(() => resetState(), 1200);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
handleProcessError(
|
|
181
|
+
response?.data?.error?.message ||
|
|
182
|
+
"Face not recognized. Please try again."
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
handleProcessError(
|
|
187
|
+
"Connection error. Please check your network.",
|
|
188
|
+
error
|
|
189
|
+
);
|
|
235
190
|
}
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
[
|
|
194
|
+
convertImageToBase64,
|
|
195
|
+
notifyMessage,
|
|
196
|
+
qrscan,
|
|
197
|
+
resetState,
|
|
198
|
+
updateState,
|
|
199
|
+
validateApiUrl,
|
|
200
|
+
safeCallback,
|
|
201
|
+
]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const handleStartFaceScan = useCallback(() => {
|
|
205
|
+
updateState({
|
|
206
|
+
currentStep: "Identity Verification",
|
|
207
|
+
animationState: ANIMATION_STATES.FACE_SCAN,
|
|
208
|
+
});
|
|
209
|
+
navigation.navigate("CCaptureImageWithoutEdit", {
|
|
210
|
+
facedetection: true,
|
|
211
|
+
cameratype: "front",
|
|
212
|
+
onSelect: uploadFaceScan,
|
|
213
|
+
});
|
|
214
|
+
}, [navigation, updateState, uploadFaceScan]);
|
|
215
|
+
|
|
216
|
+
const startQRCodeScan = useCallback(() => {
|
|
217
|
+
updateState({
|
|
218
|
+
currentStep: "Location Verification",
|
|
219
|
+
animationState: ANIMATION_STATES.QR_SCAN,
|
|
220
|
+
});
|
|
221
|
+
navigation.navigate("CCaptureImageWithoutEdit", {
|
|
222
|
+
hidebuttons: true,
|
|
223
|
+
cameratype: "back",
|
|
224
|
+
cameramoduletype: 2,
|
|
225
|
+
onSelect: handleQRScanned,
|
|
226
|
+
});
|
|
227
|
+
}, [navigation, updateState]);
|
|
228
|
+
|
|
229
|
+
const handleQRScanned = useCallback(
|
|
230
|
+
async (qrCodeData) => {
|
|
231
|
+
if (!validateApiUrl()) return;
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
updateState({ modalVisible: false });
|
|
242
|
-
resetState();
|
|
243
|
-
}, 1500);
|
|
244
|
-
} else {
|
|
245
|
-
notifyMessage(`Location mismatch (${distance.toFixed(0)}m away).`, 'error');
|
|
246
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
247
|
-
resetState();
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
notifyMessage('Invalid coordinates in QR code.', 'error');
|
|
251
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
252
|
-
resetState();
|
|
253
|
-
}
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.error("QR scan handling failed:", error);
|
|
256
|
-
notifyMessage('Unable to verify location. Please try again.', 'error');
|
|
257
|
-
updateState({ animationState: ANIMATION_STATES.ERROR });
|
|
258
|
-
resetState();
|
|
259
|
-
} finally {
|
|
260
|
-
updateState({ isLoading: false });
|
|
261
|
-
}
|
|
262
|
-
}, [callback, getCurrentLocation, notifyMessage, requestLocationPermission, resetState, updateState, validateApiUrl]);
|
|
263
|
-
|
|
264
|
-
const startProcess = useCallback(() => {
|
|
265
|
-
startCountdown(COUNTDOWN_DURATION, handleCountdownFinish);
|
|
266
|
-
handleStartFaceScan();
|
|
267
|
-
}, [handleCountdownFinish, handleStartFaceScan, startCountdown]);
|
|
268
|
-
|
|
269
|
-
useEffect(() => {
|
|
270
|
-
if (data) {
|
|
271
|
-
updateState({ modalVisible: true });
|
|
272
|
-
startProcess();
|
|
273
|
-
}
|
|
274
|
-
}, [data, startProcess, updateState]);
|
|
275
|
-
|
|
276
|
-
return (
|
|
277
|
-
<Modal
|
|
278
|
-
visible={state.modalVisible}
|
|
279
|
-
animationType="slide"
|
|
280
|
-
transparent
|
|
281
|
-
onRequestClose={() => {
|
|
282
|
-
updateState({ modalVisible: false });
|
|
283
|
-
resetState();
|
|
284
|
-
}}
|
|
285
|
-
statusBarTranslucent={true}
|
|
286
|
-
>
|
|
287
|
-
<View style={styles.modalBg}>
|
|
288
|
-
<TouchableOpacity
|
|
289
|
-
style={styles.close}
|
|
290
|
-
onPress={() => {
|
|
291
|
-
updateState({ modalVisible: false });
|
|
292
|
-
resetState();
|
|
293
|
-
}}
|
|
294
|
-
accessibilityLabel="Close modal"
|
|
295
|
-
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
296
|
-
>
|
|
297
|
-
<Icon name="close" size={24} color={COLORS.light} />
|
|
298
|
-
</TouchableOpacity>
|
|
299
|
-
|
|
300
|
-
<Text style={styles.title}>Biometric Verification</Text>
|
|
301
|
-
<Text style={styles.subTitle}>{state.currentStep}</Text>
|
|
302
|
-
|
|
303
|
-
<StateIndicator state={state.animationState} size={120} />
|
|
304
|
-
|
|
305
|
-
{state.employeeData && (
|
|
306
|
-
<EmployeeCard
|
|
307
|
-
employeeData={state.employeeData}
|
|
308
|
-
apiurl={apiurl}
|
|
309
|
-
/>
|
|
310
|
-
)}
|
|
233
|
+
updateState({
|
|
234
|
+
animationState: ANIMATION_STATES.PROCESSING,
|
|
235
|
+
isLoading: true,
|
|
236
|
+
});
|
|
311
237
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
238
|
+
try {
|
|
239
|
+
const hasPermission = await requestLocationPermission();
|
|
240
|
+
if (!hasPermission) {
|
|
241
|
+
handleProcessError("Location permission not granted.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
317
244
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
245
|
+
const qrString =
|
|
246
|
+
typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
|
|
247
|
+
if (!qrString || typeof qrString !== "string") {
|
|
248
|
+
handleProcessError("Invalid QR code. Please try again.");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
322
251
|
|
|
323
|
-
|
|
324
|
-
|
|
252
|
+
const location = await getCurrentLocation();
|
|
253
|
+
const [latStr, lngStr] = qrString.split(",");
|
|
254
|
+
const lat = parseFloat(latStr);
|
|
255
|
+
const lng = parseFloat(lngStr);
|
|
256
|
+
|
|
257
|
+
const validCoords = !isNaN(lat) && !isNaN(lng);
|
|
258
|
+
const validDev =
|
|
259
|
+
!isNaN(location?.latitude) && !isNaN(location?.longitude);
|
|
260
|
+
|
|
261
|
+
if (validCoords && validDev) {
|
|
262
|
+
const distance = getDistanceInMeters(
|
|
263
|
+
lat,
|
|
264
|
+
lng,
|
|
265
|
+
location.latitude,
|
|
266
|
+
location.longitude
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (distance <= MAX_DISTANCE_METERS) {
|
|
270
|
+
safeCallback(responseRef.current);
|
|
271
|
+
notifyMessage("Location verified successfully!", "success");
|
|
272
|
+
updateState({
|
|
273
|
+
animationState: ANIMATION_STATES.SUCCESS,
|
|
274
|
+
isLoading: false,
|
|
275
|
+
});
|
|
276
|
+
setTimeout(() => resetState(), 1200);
|
|
277
|
+
} else {
|
|
278
|
+
handleProcessError(
|
|
279
|
+
`Location mismatch (${distance.toFixed(0)}m away).`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
handleProcessError("Invalid coordinates in QR code.");
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
handleProcessError(
|
|
287
|
+
"Unable to verify location. Please try again.",
|
|
288
|
+
error
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
[
|
|
293
|
+
getCurrentLocation,
|
|
294
|
+
notifyMessage,
|
|
295
|
+
requestLocationPermission,
|
|
296
|
+
resetState,
|
|
297
|
+
updateState,
|
|
298
|
+
validateApiUrl,
|
|
299
|
+
safeCallback,
|
|
300
|
+
]
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const startProcess = useCallback(() => {
|
|
304
|
+
startCountdown(COUNTDOWN_DURATION, handleCountdownFinish);
|
|
305
|
+
handleStartFaceScan();
|
|
306
|
+
}, [handleCountdownFinish, handleStartFaceScan, startCountdown]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (data && data !== lastDataRef.current) {
|
|
310
|
+
console.log("📥 New donor data received:", data);
|
|
311
|
+
lastDataRef.current = data;
|
|
312
|
+
setModalVisible(true);
|
|
313
|
+
startProcess();
|
|
314
|
+
}
|
|
315
|
+
}, [data, startProcess]);
|
|
316
|
+
|
|
317
|
+
const loaderSource = useMemo(
|
|
318
|
+
() =>
|
|
319
|
+
state.isLoading &&
|
|
320
|
+
getLoaderGif(state.animationState, state.currentStep, apiurl),
|
|
321
|
+
[state.isLoading, state.animationState, state.currentStep, apiurl]
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<>
|
|
326
|
+
{modalVisible && (
|
|
327
|
+
<Modal
|
|
328
|
+
visible={modalVisible}
|
|
329
|
+
animationType="slide"
|
|
330
|
+
transparent
|
|
331
|
+
onRequestClose={resetState}
|
|
332
|
+
statusBarTranslucent
|
|
333
|
+
>
|
|
334
|
+
<View style={styles.modalBg}>
|
|
335
|
+
<TouchableOpacity
|
|
336
|
+
style={styles.close}
|
|
337
|
+
onPress={resetState}
|
|
338
|
+
accessibilityLabel="Close modal"
|
|
339
|
+
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
340
|
+
>
|
|
341
|
+
<Icon name="close" size={24} color={COLORS.light} />
|
|
342
|
+
</TouchableOpacity>
|
|
343
|
+
|
|
344
|
+
<Text style={styles.title}>Biometric Verification</Text>
|
|
345
|
+
<Text style={styles.subTitle}>{state.currentStep}</Text>
|
|
346
|
+
|
|
347
|
+
<StateIndicator state={state.animationState} size={120} />
|
|
348
|
+
|
|
349
|
+
{state.employeeData && (
|
|
350
|
+
<EmployeeCard
|
|
351
|
+
employeeData={state.employeeData}
|
|
352
|
+
apiurl={apiurl}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
<Notification
|
|
357
|
+
notification={notification}
|
|
358
|
+
fadeAnim={fadeAnim}
|
|
359
|
+
slideAnim={slideAnim}
|
|
360
|
+
/>
|
|
361
|
+
|
|
362
|
+
<CountdownTimer
|
|
363
|
+
duration={COUNTDOWN_DURATION}
|
|
364
|
+
currentTime={countdown}
|
|
365
|
+
/>
|
|
366
|
+
|
|
367
|
+
{loaderSource && <Loader source={loaderSource} />}
|
|
368
|
+
</View>
|
|
369
|
+
</Modal>
|
|
325
370
|
)}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
371
|
+
</>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
);
|
|
330
375
|
|
|
331
376
|
export default BiometricVerificationModal;
|