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