react-native-biometric-verifier 0.0.55 → 0.0.57
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 +4 -16
- package/src/components/CaptureImageWithoutEdit.js +3 -4
- package/src/components/Loader.js +84 -96
- package/src/components/Notification.js +38 -14
- package/src/hooks/useFaceDetectionFrameProcessor.js +26 -26
- package/src/hooks/useGeolocation.js +46 -224
- package/src/index.js +430 -560
- package/src/utils/Global.js +0 -1
- package/src/hooks/useBluetoothService.js +0 -195
- package/src/hooks/useWifiService.js +0 -175
package/src/index.js
CHANGED
|
@@ -3,8 +3,7 @@ import React, {
|
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
5
|
useCallback,
|
|
6
|
-
|
|
7
|
-
forwardRef,
|
|
6
|
+
useMemo,
|
|
8
7
|
} from "react";
|
|
9
8
|
import {
|
|
10
9
|
View,
|
|
@@ -17,8 +16,7 @@ import {
|
|
|
17
16
|
Animated,
|
|
18
17
|
} from "react-native";
|
|
19
18
|
import Icon from "react-native-vector-icons/MaterialIcons";
|
|
20
|
-
|
|
21
|
-
// Custom hooks - Removed unnecessary ones
|
|
19
|
+
// Custom hooks
|
|
22
20
|
import { useCountdown } from "./hooks/useCountdown";
|
|
23
21
|
import { useGeolocation } from "./hooks/useGeolocation";
|
|
24
22
|
import { useImageProcessing } from "./hooks/useImageProcessing";
|
|
@@ -26,6 +24,7 @@ import { useNotifyMessage } from "./hooks/useNotifyMessage";
|
|
|
26
24
|
import { useSafeCallback } from "./hooks/useSafeCallback";
|
|
27
25
|
|
|
28
26
|
// Utils
|
|
27
|
+
import { getDistanceInMeters } from "./utils/distanceCalculator";
|
|
29
28
|
import { Global } from "./utils/Global";
|
|
30
29
|
import networkServiceCall from "./utils/NetworkServiceCall";
|
|
31
30
|
import { getLoaderGif } from "./utils/getLoaderGif";
|
|
@@ -38,629 +37,511 @@ import { Notification } from "./components/Notification";
|
|
|
38
37
|
import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
|
|
39
38
|
import StepIndicator from "./components/StepIndicator";
|
|
40
39
|
|
|
41
|
-
const BiometricModal =
|
|
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 = 30,
|
|
54
|
-
duration = 100,
|
|
55
|
-
antispooflevel,
|
|
56
|
-
}, ref) => {
|
|
57
|
-
// Custom hooks - Initialize notification hook first
|
|
58
|
-
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
59
|
-
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
60
|
-
|
|
61
|
-
// Only keep geolocation hook
|
|
62
|
-
const {
|
|
63
|
-
requestLocationPermission,
|
|
64
|
-
getCurrentLocation,
|
|
65
|
-
stopLocationWatching,
|
|
66
|
-
calculateSafeDistance,
|
|
67
|
-
} = useGeolocation(notifyMessage);
|
|
68
|
-
|
|
69
|
-
const { convertImageToBase64 } = useImageProcessing();
|
|
70
|
-
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
71
|
-
|
|
72
|
-
// State - Simplified
|
|
73
|
-
const [modalVisible, setModalVisible] = useState(false);
|
|
74
|
-
const [cameraType, setCameraType] = useState("back");
|
|
75
|
-
const [state, setState] = useState({
|
|
76
|
-
isLoading: false,
|
|
77
|
-
loadingType: Global.LoadingTypes.none,
|
|
78
|
-
currentStep: "Start",
|
|
79
|
-
employeeData: null,
|
|
80
|
-
animationState: Global.AnimationStates.qrScan,
|
|
81
|
-
qrData: null,
|
|
82
|
-
// Removed: wifiReferenceScan
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Refs
|
|
86
|
-
const dataRef = useRef(data);
|
|
87
|
-
const mountedRef = useRef(true);
|
|
88
|
-
const responseRef = useRef(null);
|
|
89
|
-
const processedRef = useRef(false);
|
|
90
|
-
const resetTimeoutRef = useRef(null);
|
|
91
|
-
// Removed: bleScanTimeoutRef, wifiScanTimeoutRef
|
|
92
|
-
|
|
93
|
-
// Animation values
|
|
94
|
-
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
95
|
-
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
96
|
-
|
|
97
|
-
// Expose methods to parent via ref
|
|
98
|
-
useImperativeHandle(ref, () => ({
|
|
99
|
-
reset: resetState,
|
|
100
|
-
start: startProcess,
|
|
101
|
-
close: resetState,
|
|
102
|
-
getStatus: () => state,
|
|
103
|
-
}));
|
|
104
|
-
|
|
105
|
-
// Cleanup on unmount - Simplified
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
return () => {
|
|
108
|
-
mountedRef.current = false;
|
|
40
|
+
const BiometricModal = React.memo(
|
|
41
|
+
({ data, depKey, qrscan = false, callback, apiurl, onclose, frameProcessorFps, livenessLevel, fileurl, imageurl, navigation, duration = 100, MaxDistanceMeters = 30, antispooflevel }) => {
|
|
109
42
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
43
|
+
// Custom hooks
|
|
44
|
+
const { countdown, startCountdown, resetCountdown, pauseCountdown, resumeCountdown } = useCountdown(duration);
|
|
45
|
+
const { requestLocationPermission, getCurrentLocation } = useGeolocation();
|
|
46
|
+
const { convertImageToBase64 } = useImageProcessing();
|
|
47
|
+
const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } = useNotifyMessage();
|
|
48
|
+
const safeCallback = useSafeCallback(callback, notifyMessage);
|
|
113
49
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
50
|
+
// State
|
|
51
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
52
|
+
const [cameraType, setCameraType] = useState("front");
|
|
53
|
+
const [state, setState] = useState({
|
|
54
|
+
isLoading: false,
|
|
55
|
+
loadingType: Global.LoadingTypes.none,
|
|
56
|
+
currentStep: "Start",
|
|
57
|
+
employeeData: null,
|
|
58
|
+
animationState: Global.AnimationStates.faceScan,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Refs
|
|
62
|
+
const dataRef = useRef(data);
|
|
63
|
+
const mountedRef = useRef(true);
|
|
64
|
+
const responseRef = useRef(null);
|
|
65
|
+
const processedRef = useRef(false);
|
|
66
|
+
const resetTimeoutRef = useRef(null);
|
|
67
|
+
|
|
68
|
+
// Animation values
|
|
69
|
+
const iconScaleAnim = useRef(new Animated.Value(1)).current;
|
|
70
|
+
const iconOpacityAnim = useRef(new Animated.Value(0)).current;
|
|
71
|
+
|
|
72
|
+
// Cleanup on unmount
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
mountedRef.current = false;
|
|
76
|
+
|
|
77
|
+
if (resetTimeoutRef.current) {
|
|
78
|
+
clearTimeout(resetTimeoutRef.current);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearNotification();
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Update dataRef when data changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
dataRef.current = data;
|
|
88
|
+
}, [data]);
|
|
89
|
+
|
|
90
|
+
// Animation helper
|
|
91
|
+
const animateIcon = useCallback(() => {
|
|
92
|
+
// Reset animation
|
|
93
|
+
iconScaleAnim.setValue(1);
|
|
94
|
+
iconOpacityAnim.setValue(0);
|
|
95
|
+
|
|
96
|
+
// Start animation sequence
|
|
97
|
+
Animated.sequence([
|
|
98
|
+
Animated.parallel([
|
|
99
|
+
Animated.timing(iconOpacityAnim, {
|
|
100
|
+
toValue: 1,
|
|
101
|
+
duration: 300,
|
|
102
|
+
useNativeDriver: true,
|
|
103
|
+
}),
|
|
104
|
+
Animated.spring(iconScaleAnim, {
|
|
105
|
+
toValue: 1.2,
|
|
106
|
+
friction: 3,
|
|
107
|
+
useNativeDriver: true,
|
|
108
|
+
}),
|
|
109
|
+
]),
|
|
136
110
|
Animated.spring(iconScaleAnim, {
|
|
137
|
-
toValue: 1
|
|
138
|
-
friction:
|
|
111
|
+
toValue: 1,
|
|
112
|
+
friction: 5,
|
|
139
113
|
useNativeDriver: true,
|
|
140
114
|
}),
|
|
141
|
-
])
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (newState.isLoading) {
|
|
159
|
-
pauseCountdown();
|
|
160
|
-
} else {
|
|
161
|
-
resumeCountdown();
|
|
115
|
+
]).start();
|
|
116
|
+
}, [iconScaleAnim, iconOpacityAnim]);
|
|
117
|
+
|
|
118
|
+
// State update helper
|
|
119
|
+
const updateState = useCallback((newState) => {
|
|
120
|
+
if (mountedRef.current) {
|
|
121
|
+
setState((prev) => {
|
|
122
|
+
const merged = { ...prev, ...newState };
|
|
123
|
+
|
|
124
|
+
if (JSON.stringify(prev) !== JSON.stringify(merged)) {
|
|
125
|
+
// Pause/resume countdown based on loading state
|
|
126
|
+
if (newState.isLoading !== undefined) {
|
|
127
|
+
if (newState.isLoading) {
|
|
128
|
+
pauseCountdown();
|
|
129
|
+
} else {
|
|
130
|
+
resumeCountdown();
|
|
131
|
+
}
|
|
162
132
|
}
|
|
163
|
-
}
|
|
164
133
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
134
|
+
// Animate icon when step changes
|
|
135
|
+
if (newState.currentStep && newState.currentStep !== prev.currentStep) {
|
|
136
|
+
animateIcon();
|
|
137
|
+
}
|
|
168
138
|
|
|
169
|
-
|
|
170
|
-
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
171
141
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
142
|
+
return prev;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}, [animateIcon, pauseCountdown, resumeCountdown]);
|
|
176
146
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (onclose) {
|
|
147
|
+
// Reset state helper
|
|
148
|
+
const resetState = useCallback(() => {
|
|
180
149
|
onclose(false);
|
|
181
|
-
}
|
|
182
150
|
|
|
183
|
-
|
|
184
|
-
isLoading: false,
|
|
185
|
-
loadingType: Global.LoadingTypes.none,
|
|
186
|
-
currentStep: "Start",
|
|
187
|
-
employeeData: null,
|
|
188
|
-
animationState: Global.AnimationStates.qrScan,
|
|
189
|
-
qrData: null,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
setModalVisible(false);
|
|
193
|
-
processedRef.current = false;
|
|
194
|
-
resetCountdown();
|
|
195
|
-
stopLocationWatching();
|
|
196
|
-
clearNotification();
|
|
197
|
-
|
|
198
|
-
if (resetTimeoutRef.current) {
|
|
199
|
-
clearTimeout(resetTimeoutRef.current);
|
|
200
|
-
resetTimeoutRef.current = null;
|
|
201
|
-
}
|
|
202
|
-
}, [resetCountdown, stopLocationWatching, clearNotification, onclose]);
|
|
203
|
-
|
|
204
|
-
// Error handler
|
|
205
|
-
const handleProcessError = useCallback(
|
|
206
|
-
(message, errorObj = null) => {
|
|
207
|
-
if (errorObj) {
|
|
208
|
-
console.error("Process Error:", errorObj);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
notifyMessage(message, "error");
|
|
212
|
-
updateState({
|
|
213
|
-
animationState: Global.AnimationStates.error,
|
|
151
|
+
setState({
|
|
214
152
|
isLoading: false,
|
|
215
153
|
loadingType: Global.LoadingTypes.none,
|
|
154
|
+
currentStep: "Start",
|
|
155
|
+
employeeData: null,
|
|
156
|
+
animationState: Global.AnimationStates.faceScan,
|
|
216
157
|
});
|
|
217
158
|
|
|
159
|
+
setModalVisible(false);
|
|
160
|
+
processedRef.current = false;
|
|
161
|
+
resetCountdown();
|
|
162
|
+
clearNotification();
|
|
163
|
+
|
|
218
164
|
if (resetTimeoutRef.current) {
|
|
219
165
|
clearTimeout(resetTimeoutRef.current);
|
|
166
|
+
resetTimeoutRef.current = null;
|
|
220
167
|
}
|
|
168
|
+
}, [resetCountdown, clearNotification]);
|
|
221
169
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Countdown finish handler
|
|
230
|
-
const handleCountdownFinish = useCallback(() => {
|
|
231
|
-
handleProcessError("Time is up! Please try again.");
|
|
232
|
-
|
|
233
|
-
if (navigation?.canGoBack?.()) {
|
|
234
|
-
navigation.goBack();
|
|
235
|
-
}
|
|
236
|
-
}, [handleProcessError, navigation]);
|
|
237
|
-
|
|
238
|
-
// API URL validation
|
|
239
|
-
const validateApiUrl = useCallback(() => {
|
|
240
|
-
if (!apiurl || typeof apiurl !== "string") {
|
|
241
|
-
handleProcessError("Invalid API URL configuration.");
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return true;
|
|
246
|
-
}, [apiurl, handleProcessError]);
|
|
247
|
-
|
|
248
|
-
// Simplified QR scanning handler - GPS + Key verification only
|
|
249
|
-
const handleQRScanned = useCallback(
|
|
250
|
-
async (qrCodeData) => {
|
|
251
|
-
if (!validateApiUrl()) return;
|
|
252
|
-
|
|
253
|
-
updateState({
|
|
254
|
-
animationState: Global.AnimationStates.processing,
|
|
255
|
-
isLoading: true,
|
|
256
|
-
loadingType: Global.LoadingTypes.locationVerification,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
// 1. Request location permission
|
|
261
|
-
updateState({ loadingType: Global.LoadingTypes.locationPermission });
|
|
262
|
-
const hasLocationPermission = await requestLocationPermission();
|
|
263
|
-
if (!hasLocationPermission) {
|
|
264
|
-
handleProcessError("Location permission not granted.");
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 2. Parse QR data with validation
|
|
269
|
-
const qrString = typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
|
|
270
|
-
if (!qrString || typeof qrString !== "string") {
|
|
271
|
-
handleProcessError("Invalid QR code. Please try again.");
|
|
272
|
-
return;
|
|
170
|
+
// Error handler
|
|
171
|
+
const handleProcessError = useCallback(
|
|
172
|
+
(message, errorObj = null) => {
|
|
173
|
+
if (errorObj) {
|
|
174
|
+
console.error("Process Error:", errorObj);
|
|
273
175
|
}
|
|
274
176
|
|
|
275
|
-
|
|
276
|
-
|
|
177
|
+
notifyMessage(message, "error");
|
|
178
|
+
updateState({
|
|
179
|
+
animationState: Global.AnimationStates.error,
|
|
180
|
+
isLoading: false,
|
|
181
|
+
loadingType: Global.LoadingTypes.none,
|
|
182
|
+
});
|
|
277
183
|
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
return;
|
|
184
|
+
if (resetTimeoutRef.current) {
|
|
185
|
+
clearTimeout(resetTimeoutRef.current);
|
|
281
186
|
}
|
|
282
187
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
188
|
+
resetTimeoutRef.current = setTimeout(() => {
|
|
189
|
+
resetState();
|
|
190
|
+
}, 1200);
|
|
191
|
+
},
|
|
192
|
+
[notifyMessage, resetState, updateState]
|
|
193
|
+
);
|
|
287
194
|
|
|
288
|
-
|
|
289
|
-
|
|
195
|
+
// Countdown finish handler
|
|
196
|
+
const handleCountdownFinish = useCallback(() => {
|
|
197
|
+
handleProcessError("Time is up! Please try again.");
|
|
290
198
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
199
|
+
if (navigation.canGoBack()) {
|
|
200
|
+
navigation.goBack();
|
|
201
|
+
}
|
|
202
|
+
}, [handleProcessError, navigation]);
|
|
295
203
|
|
|
296
|
-
|
|
297
|
-
|
|
204
|
+
// API URL validation
|
|
205
|
+
const validateApiUrl = useCallback(() => {
|
|
206
|
+
if (!apiurl || typeof apiurl !== "string") {
|
|
207
|
+
handleProcessError("Invalid API URL configuration.");
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
298
210
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
location = await getCurrentLocation();
|
|
302
|
-
} catch (locationError) {
|
|
303
|
-
console.error('Location error:', locationError);
|
|
304
|
-
handleProcessError("Failed to get location. Please try again.");
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
211
|
+
return true;
|
|
212
|
+
}, [apiurl, handleProcessError]);
|
|
307
213
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
214
|
+
// Face scan upload
|
|
215
|
+
const uploadFaceScan = useCallback(
|
|
216
|
+
async (selfie) => {
|
|
217
|
+
if (!validateApiUrl()) return;
|
|
218
|
+
const currentData = dataRef.current;
|
|
313
219
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
isNaN(location.latitude) || isNaN(location.longitude)) {
|
|
317
|
-
handleProcessError("Invalid GPS coordinates received.");
|
|
220
|
+
if (!currentData) {
|
|
221
|
+
handleProcessError("Employee data not found.");
|
|
318
222
|
return;
|
|
319
223
|
}
|
|
320
224
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// 5. Calculate distance using safe calculation
|
|
327
|
-
updateState({ loadingType: Global.LoadingTypes.calculateDistance });
|
|
328
|
-
|
|
329
|
-
const distance = calculateSafeDistance(
|
|
330
|
-
{ latitude: qrLat, longitude: qrLng },
|
|
331
|
-
{ latitude: location.latitude, longitude: location.longitude }
|
|
332
|
-
);
|
|
225
|
+
updateState({
|
|
226
|
+
isLoading: true,
|
|
227
|
+
loadingType: Global.LoadingTypes.faceRecognition,
|
|
228
|
+
animationState: Global.AnimationStates.processing,
|
|
229
|
+
});
|
|
333
230
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
handleProcessError("Failed to calculate distance. Please try again.");
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
231
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
232
|
+
let base64;
|
|
339
233
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (verificationPassed) {
|
|
346
|
-
const locationDetails = {
|
|
347
|
-
qrLocation: {
|
|
348
|
-
latitude: qrLat,
|
|
349
|
-
longitude: qrLng,
|
|
350
|
-
accuracy: qrAccuracy
|
|
351
|
-
},
|
|
352
|
-
deviceLocation: {
|
|
353
|
-
latitude: location.latitude,
|
|
354
|
-
longitude: location.longitude,
|
|
355
|
-
altitude: location.altitude || 0,
|
|
356
|
-
accuracy: location.accuracy,
|
|
357
|
-
speed: location.speed || 0,
|
|
358
|
-
heading: location.heading || 0,
|
|
359
|
-
timestamp: location.timestamp || Date.now()
|
|
360
|
-
},
|
|
361
|
-
distanceMeters: distance,
|
|
362
|
-
accuracyBuffer: MaxDistanceMeters,
|
|
363
|
-
verified: true,
|
|
364
|
-
verificationMethod: "GPS+Key",
|
|
365
|
-
verifiedAt: new Date().toISOString(),
|
|
366
|
-
locationMethod: location.provider || 'unknown',
|
|
367
|
-
qrData: qrString,
|
|
368
|
-
qrKey: qrDepKey
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
responseRef.current = { location: locationDetails };
|
|
234
|
+
try {
|
|
235
|
+
updateState({
|
|
236
|
+
loadingType: Global.LoadingTypes.imageProcessing,
|
|
237
|
+
});
|
|
372
238
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
239
|
+
base64 = await convertImageToBase64(selfie?.uri);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error("Image conversion failed:", err);
|
|
242
|
+
handleProcessError("Image conversion failed.", err);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
379
245
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
let errorMsg = `Verification failed: ${distance.toFixed(1)}m away`;
|
|
385
|
-
if (qrDepKey !== depkey) errorMsg += " (Key mismatch)";
|
|
386
|
-
handleProcessError(errorMsg);
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error("Location verification failed:", error);
|
|
390
|
-
handleProcessError("Unable to verify location. Please try again.", error);
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
[
|
|
394
|
-
validateApiUrl,
|
|
395
|
-
updateState,
|
|
396
|
-
requestLocationPermission,
|
|
397
|
-
getCurrentLocation,
|
|
398
|
-
notifyMessage,
|
|
399
|
-
handleProcessError,
|
|
400
|
-
startFaceRecognition,
|
|
401
|
-
depkey,
|
|
402
|
-
MaxDistanceMeters,
|
|
403
|
-
calculateSafeDistance,
|
|
404
|
-
]
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
// Face scan upload - SECOND STEP (simplified, no WiFi revalidation)
|
|
408
|
-
const uploadFaceScan = useCallback(
|
|
409
|
-
async (selfie) => {
|
|
410
|
-
if (!validateApiUrl()) return;
|
|
411
|
-
|
|
412
|
-
// Check if QR scan was completed successfully
|
|
413
|
-
if (!state.qrData) {
|
|
414
|
-
handleProcessError("Please complete QR scan first.");
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
246
|
+
if (!base64) {
|
|
247
|
+
handleProcessError("Failed to process image.");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
417
250
|
|
|
418
|
-
|
|
251
|
+
try {
|
|
252
|
+
const body = { image: base64 };
|
|
253
|
+
const header = { faceid: currentData };
|
|
254
|
+
const buttonapi = `${apiurl}python/recognize`;
|
|
419
255
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
256
|
+
updateState({
|
|
257
|
+
loadingType: Global.LoadingTypes.networkRequest,
|
|
258
|
+
});
|
|
424
259
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
260
|
+
const response = await networkServiceCall(
|
|
261
|
+
"POST",
|
|
262
|
+
buttonapi,
|
|
263
|
+
header,
|
|
264
|
+
body
|
|
265
|
+
);
|
|
430
266
|
|
|
431
|
-
|
|
432
|
-
|
|
267
|
+
if (response?.httpstatus === 200) {
|
|
268
|
+
responseRef.current = {
|
|
269
|
+
...responseRef.current,
|
|
270
|
+
faceRecognition: response.data?.data || null,
|
|
271
|
+
};
|
|
272
|
+
updateState({
|
|
273
|
+
employeeData: response.data?.data || null,
|
|
274
|
+
animationState: Global.AnimationStates.success,
|
|
275
|
+
isLoading: false,
|
|
276
|
+
loadingType: Global.LoadingTypes.none,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
notifyMessage("Identity verified successfully!", "success");
|
|
280
|
+
|
|
281
|
+
safeCallback(responseRef.current);
|
|
282
|
+
|
|
283
|
+
if (resetTimeoutRef.current) {
|
|
284
|
+
clearTimeout(resetTimeoutRef.current);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
resetTimeoutRef.current = setTimeout(() => {
|
|
288
|
+
resetState();
|
|
289
|
+
}, 1200);
|
|
290
|
+
} else {
|
|
291
|
+
handleProcessError(
|
|
292
|
+
response?.data?.message ||
|
|
293
|
+
"Face not recognized. Please try again."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error("Network request failed:", error);
|
|
298
|
+
handleProcessError(
|
|
299
|
+
"Connection error. Please check your network.",
|
|
300
|
+
error
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
[
|
|
306
|
+
convertImageToBase64,
|
|
307
|
+
notifyMessage,
|
|
308
|
+
qrscan,
|
|
309
|
+
resetState,
|
|
310
|
+
updateState,
|
|
311
|
+
validateApiUrl,
|
|
312
|
+
safeCallback,
|
|
313
|
+
handleProcessError
|
|
314
|
+
]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// QR code processing
|
|
318
|
+
const handleQRScanned = useCallback(
|
|
319
|
+
async (qrCodeData) => {
|
|
320
|
+
if (!validateApiUrl()) return;
|
|
321
|
+
|
|
322
|
+
updateState({
|
|
323
|
+
animationState: Global.AnimationStates.processing,
|
|
324
|
+
isLoading: true,
|
|
325
|
+
loadingType: Global.LoadingTypes.locationVerification,
|
|
326
|
+
});
|
|
433
327
|
|
|
434
328
|
try {
|
|
435
329
|
updateState({
|
|
436
|
-
loadingType: Global.LoadingTypes.
|
|
330
|
+
loadingType: Global.LoadingTypes.locationPermission,
|
|
437
331
|
});
|
|
438
332
|
|
|
439
|
-
|
|
440
|
-
} catch (err) {
|
|
441
|
-
console.error("Image conversion failed:", err);
|
|
442
|
-
handleProcessError("Image conversion failed.", err);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
333
|
+
const hasPermission = await requestLocationPermission();
|
|
445
334
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
335
|
+
if (!hasPermission) {
|
|
336
|
+
handleProcessError("Location permission not granted.");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
450
339
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
340
|
+
const qrString =
|
|
341
|
+
typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
|
|
342
|
+
|
|
343
|
+
if (!qrString || typeof qrString !== "string") {
|
|
344
|
+
handleProcessError("Invalid QR code. Please try again.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
455
347
|
|
|
456
348
|
updateState({
|
|
457
|
-
loadingType: Global.LoadingTypes.
|
|
349
|
+
loadingType: Global.LoadingTypes.gettingLocation,
|
|
458
350
|
});
|
|
459
351
|
|
|
460
|
-
const
|
|
461
|
-
"POST",
|
|
462
|
-
buttonapi,
|
|
463
|
-
header,
|
|
464
|
-
body
|
|
465
|
-
);
|
|
352
|
+
const location = await getCurrentLocation();
|
|
466
353
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
};
|
|
354
|
+
const [latStr, lngStr, qrKey] = qrString.split(",");
|
|
355
|
+
const lat = parseFloat(latStr);
|
|
356
|
+
const lng = parseFloat(lngStr);
|
|
357
|
+
const validCoords = !isNaN(lat) && !isNaN(lng);
|
|
358
|
+
const validDev =
|
|
359
|
+
!isNaN(location?.latitude) && !isNaN(location?.longitude);
|
|
474
360
|
|
|
361
|
+
if (validCoords && validDev) {
|
|
475
362
|
updateState({
|
|
476
|
-
|
|
477
|
-
animationState: Global.AnimationStates.success,
|
|
478
|
-
isLoading: false,
|
|
479
|
-
loadingType: Global.LoadingTypes.none,
|
|
363
|
+
loadingType: Global.LoadingTypes.calculateDistance,
|
|
480
364
|
});
|
|
481
365
|
|
|
482
|
-
|
|
366
|
+
const distance = getDistanceInMeters(
|
|
367
|
+
lat,
|
|
368
|
+
lng,
|
|
369
|
+
location.latitude,
|
|
370
|
+
location.longitude
|
|
371
|
+
);
|
|
483
372
|
|
|
484
|
-
|
|
485
|
-
|
|
373
|
+
if (distance <= MaxDistanceMeters && qrKey === depKey) {
|
|
374
|
+
responseRef.current = location
|
|
375
|
+
notifyMessage("Location verified successfully!", "success");
|
|
486
376
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
377
|
+
updateState({
|
|
378
|
+
animationState: Global.AnimationStates.success,
|
|
379
|
+
isLoading: false,
|
|
380
|
+
loadingType: Global.LoadingTypes.none,
|
|
381
|
+
});
|
|
382
|
+
setTimeout(() => handleStartFaceScan(), 1200);
|
|
490
383
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
384
|
+
} else {
|
|
385
|
+
handleProcessError(
|
|
386
|
+
`Location mismatch (${distance.toFixed(0)}m away).`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
494
389
|
} else {
|
|
495
|
-
handleProcessError(
|
|
496
|
-
response?.data?.message ||
|
|
497
|
-
"Face not recognized. Please try again."
|
|
498
|
-
);
|
|
390
|
+
handleProcessError("Invalid coordinates in QR code.");
|
|
499
391
|
}
|
|
500
392
|
} catch (error) {
|
|
501
|
-
console.error("
|
|
393
|
+
console.error("Location verification failed:", error);
|
|
502
394
|
handleProcessError(
|
|
503
|
-
"
|
|
395
|
+
"Unable to verify location. Please try again.",
|
|
504
396
|
error
|
|
505
397
|
);
|
|
506
398
|
}
|
|
399
|
+
},
|
|
400
|
+
[
|
|
401
|
+
getCurrentLocation,
|
|
402
|
+
notifyMessage,
|
|
403
|
+
requestLocationPermission,
|
|
404
|
+
resetState,
|
|
405
|
+
updateState,
|
|
406
|
+
validateApiUrl,
|
|
407
|
+
handleProcessError
|
|
408
|
+
]
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Image capture handler
|
|
412
|
+
const handleImageCapture = useCallback(
|
|
413
|
+
async (capturedData) => {
|
|
414
|
+
if (state.currentStep === "Identity Verification") {
|
|
415
|
+
uploadFaceScan(capturedData);
|
|
416
|
+
} else if (state.currentStep === "Location Verification") {
|
|
417
|
+
handleQRScanned(capturedData);
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
[state.currentStep, uploadFaceScan, handleQRScanned]
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Start face scan
|
|
424
|
+
const handleStartFaceScan = useCallback(() => {
|
|
425
|
+
updateState({
|
|
426
|
+
currentStep: "Identity Verification",
|
|
427
|
+
animationState: Global.AnimationStates.faceScan,
|
|
507
428
|
});
|
|
508
|
-
|
|
509
|
-
[
|
|
510
|
-
convertImageToBase64,
|
|
511
|
-
notifyMessage,
|
|
512
|
-
resetState,
|
|
513
|
-
updateState,
|
|
514
|
-
validateApiUrl,
|
|
515
|
-
safeCallback,
|
|
516
|
-
handleProcessError,
|
|
517
|
-
state.qrData,
|
|
518
|
-
apiurl,
|
|
519
|
-
]
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
// Image capture handler
|
|
523
|
-
const handleImageCapture = useCallback(
|
|
524
|
-
async (capturedData) => {
|
|
525
|
-
if (state.currentStep === "Location Verification") {
|
|
526
|
-
await handleQRScanned(capturedData);
|
|
527
|
-
} else if (state.currentStep === "Identity Verification") {
|
|
528
|
-
await uploadFaceScan(capturedData);
|
|
529
|
-
}
|
|
530
|
-
},
|
|
531
|
-
[state.currentStep, uploadFaceScan, handleQRScanned]
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
// Start QR code scan - FIRST STEP
|
|
535
|
-
const handleStartQRScan = useCallback(() => {
|
|
536
|
-
updateState({
|
|
537
|
-
currentStep: "Location Verification",
|
|
538
|
-
animationState: Global.AnimationStates.qrScan,
|
|
539
|
-
});
|
|
540
|
-
setCameraType("back");
|
|
541
|
-
}, [updateState]);
|
|
429
|
+
setCameraType("front");
|
|
430
|
+
}, [updateState]);
|
|
542
431
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
</View>
|
|
600
|
-
)}
|
|
601
|
-
|
|
602
|
-
{/* UI elements positioned absolutely on top of camera */}
|
|
603
|
-
<TouchableOpacity
|
|
604
|
-
style={styles.closeButton}
|
|
605
|
-
onPress={resetState}
|
|
606
|
-
accessibilityLabel="Close modal"
|
|
607
|
-
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
608
|
-
>
|
|
609
|
-
<Icon name="close" size={24} color={Global.AppTheme.light} />
|
|
610
|
-
</TouchableOpacity>
|
|
611
|
-
|
|
612
|
-
<View style={styles.topContainer}>
|
|
613
|
-
{!shouldShowCamera && (
|
|
614
|
-
<View style={styles.headerContainer}>
|
|
615
|
-
<View style={styles.titleContainer}>
|
|
616
|
-
<Text style={styles.title}>Biometric Verification</Text>
|
|
617
|
-
<Text style={styles.subtitle}>{state.currentStep}</Text>
|
|
618
|
-
</View>
|
|
432
|
+
// Start QR code scan
|
|
433
|
+
const startQRCodeScan = useCallback(() => {
|
|
434
|
+
updateState({
|
|
435
|
+
currentStep: "Location Verification",
|
|
436
|
+
animationState: Global.AnimationStates.qrScan,
|
|
437
|
+
});
|
|
438
|
+
setCameraType("back");
|
|
439
|
+
}, [updateState]);
|
|
440
|
+
|
|
441
|
+
// Start the verification process
|
|
442
|
+
const startProcess = useCallback(() => {
|
|
443
|
+
startCountdown(handleCountdownFinish);
|
|
444
|
+
if (qrscan) {
|
|
445
|
+
startQRCodeScan()
|
|
446
|
+
} else {
|
|
447
|
+
handleStartFaceScan();
|
|
448
|
+
}
|
|
449
|
+
}, [handleCountdownFinish, handleStartFaceScan, startCountdown, startQRCodeScan]);
|
|
450
|
+
|
|
451
|
+
// Open modal when data is received
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (data && !modalVisible && !processedRef.current) {
|
|
454
|
+
processedRef.current = true;
|
|
455
|
+
setModalVisible(true);
|
|
456
|
+
startProcess();
|
|
457
|
+
}
|
|
458
|
+
}, [data, modalVisible, startProcess]);
|
|
459
|
+
|
|
460
|
+
// Determine if camera should be shown
|
|
461
|
+
const shouldShowCamera =
|
|
462
|
+
(state.currentStep === "Identity Verification" ||
|
|
463
|
+
state.currentStep === "Location Verification") &&
|
|
464
|
+
state.animationState !== Global.AnimationStates.success &&
|
|
465
|
+
state.animationState !== Global.AnimationStates.error;
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<Modal
|
|
469
|
+
visible={modalVisible}
|
|
470
|
+
animationType="slide"
|
|
471
|
+
transparent
|
|
472
|
+
onRequestClose={resetState}
|
|
473
|
+
statusBarTranslucent
|
|
474
|
+
>
|
|
475
|
+
<View style={styles.modalContainer}>
|
|
476
|
+
{/* Camera component - full screen */}
|
|
477
|
+
{shouldShowCamera && !state.isLoading && (
|
|
478
|
+
<View style={styles.cameraContainer}>
|
|
479
|
+
<CaptureImageWithoutEdit
|
|
480
|
+
cameraType={cameraType}
|
|
481
|
+
onCapture={handleImageCapture}
|
|
482
|
+
showCodeScanner={state.currentStep === "Location Verification"}
|
|
483
|
+
isLoading={state.isLoading}
|
|
484
|
+
frameProcessorFps={frameProcessorFps}
|
|
485
|
+
livenessLevel={livenessLevel}
|
|
486
|
+
antispooflevel={antispooflevel}
|
|
487
|
+
/>
|
|
619
488
|
</View>
|
|
620
489
|
)}
|
|
621
|
-
</View>
|
|
622
490
|
|
|
623
|
-
|
|
624
|
-
<
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
491
|
+
{/* UI elements positioned absolutely on top of camera */}
|
|
492
|
+
<TouchableOpacity
|
|
493
|
+
style={styles.closeButton}
|
|
494
|
+
onPress={resetState}
|
|
495
|
+
accessibilityLabel="Close modal"
|
|
496
|
+
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
497
|
+
>
|
|
498
|
+
<Icon name="close" size={24} color={Global.AppTheme.light} />
|
|
499
|
+
</TouchableOpacity>
|
|
500
|
+
|
|
501
|
+
<View style={styles.topContainer}>
|
|
502
|
+
{!shouldShowCamera && (
|
|
503
|
+
<View style={styles.headerContainer}>
|
|
504
|
+
<View style={styles.titleContainer}>
|
|
505
|
+
<Text style={styles.title}>Biometric Verification</Text>
|
|
506
|
+
<Text style={styles.subtitle}>{state.currentStep}</Text>
|
|
507
|
+
</View>
|
|
508
|
+
</View>
|
|
509
|
+
)}
|
|
510
|
+
</View>
|
|
629
511
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
<Card employeeData={state.employeeData} apiurl={apiurl} fileurl={fileurl} />
|
|
512
|
+
<View style={styles.topContainerstep}>
|
|
513
|
+
<StepIndicator currentStep={state.currentStep} qrscan={qrscan} />
|
|
633
514
|
</View>
|
|
634
|
-
)}
|
|
635
515
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
516
|
+
{state.employeeData && (
|
|
517
|
+
<View style={styles.cardContainer}>
|
|
518
|
+
<Card employeeData={state.employeeData} apiurl={apiurl} fileurl={fileurl} />
|
|
519
|
+
</View>
|
|
520
|
+
)}
|
|
521
|
+
|
|
522
|
+
<View style={styles.notificationContainer}>
|
|
523
|
+
<Notification
|
|
524
|
+
notification={notification}
|
|
525
|
+
fadeAnim={fadeAnim}
|
|
526
|
+
slideAnim={slideAnim}
|
|
527
|
+
/>
|
|
528
|
+
</View>
|
|
643
529
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
530
|
+
<View style={styles.timerContainer}>
|
|
531
|
+
<CountdownTimer
|
|
532
|
+
duration={Global.CountdownDuration}
|
|
533
|
+
currentTime={countdown}
|
|
534
|
+
/>
|
|
535
|
+
</View>
|
|
536
|
+
<Loader
|
|
537
|
+
state={state}
|
|
538
|
+
gifSource={getLoaderGif(state.animationState, state.currentStep, apiurl, imageurl)}
|
|
648
539
|
/>
|
|
649
540
|
</View>
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
</View>
|
|
655
|
-
</Modal>
|
|
656
|
-
);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// Wrap with memo after forwardRef
|
|
660
|
-
const MemoizedBiometricModal = React.memo(BiometricModal);
|
|
661
|
-
|
|
662
|
-
// Add display name for debugging
|
|
663
|
-
MemoizedBiometricModal.displayName = 'BiometricModal';
|
|
541
|
+
</Modal>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
);
|
|
664
545
|
|
|
665
546
|
const styles = StyleSheet.create({
|
|
666
547
|
modalContainer: {
|
|
@@ -687,7 +568,6 @@ const styles = StyleSheet.create({
|
|
|
687
568
|
position: 'absolute',
|
|
688
569
|
bottom: Platform.OS === 'ios' ? 50 : 30,
|
|
689
570
|
left: 0,
|
|
690
|
-
right: 0,
|
|
691
571
|
zIndex: 10,
|
|
692
572
|
},
|
|
693
573
|
headerContainer: {
|
|
@@ -723,16 +603,6 @@ const styles = StyleSheet.create({
|
|
|
723
603
|
textShadowOffset: { width: 1, height: 1 },
|
|
724
604
|
textShadowRadius: 2,
|
|
725
605
|
},
|
|
726
|
-
wifiIndicator: {
|
|
727
|
-
fontSize: 12,
|
|
728
|
-
color: Global.AppTheme.primary || '#4CAF50',
|
|
729
|
-
marginTop: 4,
|
|
730
|
-
fontWeight: '500',
|
|
731
|
-
fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
|
|
732
|
-
textShadowColor: 'rgba(0, 0, 0, 0.2)',
|
|
733
|
-
textShadowOffset: { width: 1, height: 1 },
|
|
734
|
-
textShadowRadius: 1,
|
|
735
|
-
},
|
|
736
606
|
closeButton: {
|
|
737
607
|
position: 'absolute',
|
|
738
608
|
top: Platform.OS === 'ios' ? 40 : 20,
|
|
@@ -767,4 +637,4 @@ const styles = StyleSheet.create({
|
|
|
767
637
|
},
|
|
768
638
|
});
|
|
769
639
|
|
|
770
|
-
export default
|
|
640
|
+
export default BiometricModal;
|