react-native-biometric-verifier 0.0.12 โ†’ 0.0.13

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/src/index.js CHANGED
@@ -11,6 +11,9 @@ import {
11
11
  Text,
12
12
  Modal,
13
13
  InteractionManager,
14
+ StyleSheet,
15
+ Platform,
16
+ Animated,
14
17
  } from "react-native";
15
18
  import Icon from "react-native-vector-icons/MaterialIcons";
16
19
  import { useCountdown } from "./hooks/useCountdown";
@@ -23,67 +26,128 @@ import {
23
26
  COLORS,
24
27
  COUNTDOWN_DURATION,
25
28
  MAX_DISTANCE_METERS,
29
+ LOADING_TYPES,
26
30
  } from "./utils/constants";
27
31
  import Loader from "./components/Loader";
28
32
  import { CountdownTimer } from "./components/CountdownTimer";
29
- import { EmployeeCard } from "./components/EmployeeCard";
33
+ import { CCard } from "./components/CCard";
30
34
  import { Notification } from "./components/Notification";
31
- import { styles } from "./components/styles";
32
35
  import { useNavigation } from "@react-navigation/native";
33
36
  import networkServiceCall from "./utils/NetworkServiceCall";
34
37
  import { getLoaderGif } from "./utils/getLoaderGif";
35
38
  import { useSafeCallback } from "./hooks/useSafeCallback";
39
+ import CaptureImageWithoutEdit from "./components/CaptureImageWithoutEdit";
40
+ import StepIndicator from "./components/StepIndicator";
41
+ import StepIcon from "./components/StepIcon";
36
42
 
37
43
  const BiometricVerificationModal = React.memo(
38
- ({ data, qrscan = false, callback, apiurl }) => {
44
+ ({ data, qrscan = false, callback, apiurl, onclose }) => {
39
45
  const navigation = useNavigation();
40
46
  const { countdown, startCountdown, resetCountdown } = useCountdown();
41
47
  const { requestLocationPermission, getCurrentLocation } = useGeolocation();
42
48
  const { convertImageToBase64 } = useImageProcessing();
43
- const { notification, fadeAnim, slideAnim, notifyMessage } =
49
+ const { notification, fadeAnim, slideAnim, notifyMessage, clearNotification } =
44
50
  useNotifyMessage();
51
+
45
52
  const [modalVisible, setModalVisible] = useState(false);
53
+ const [cameraType, setCameraType] = useState("front");
54
+
46
55
  const [state, setState] = useState({
47
56
  isLoading: false,
57
+ loadingType: LOADING_TYPES.NONE,
48
58
  currentStep: "Start",
49
59
  employeeData: null,
50
60
  animationState: ANIMATION_STATES.FACE_SCAN,
51
61
  });
52
-
62
+
53
63
  const dataRef = useRef(data);
54
64
  const mountedRef = useRef(true);
55
65
  const responseRef = useRef(null);
56
66
  const processedRef = useRef(false);
57
67
  const safeCallback = useSafeCallback(callback, notifyMessage);
58
68
  const resetTimeoutRef = useRef(null);
69
+
70
+ // Animation values for icons
71
+ const iconScaleAnim = useRef(new Animated.Value(1)).current;
72
+ const iconOpacityAnim = useRef(new Animated.Value(0)).current;
59
73
 
60
- /** Cleanup on unmount */
74
+ // Cleanup on unmount
61
75
  useEffect(() => {
76
+ console.log("๐Ÿ”ง BiometricVerificationModal mounted");
77
+
62
78
  return () => {
79
+ console.log("๐Ÿงน BiometricVerificationModal unmounting - cleaning up");
63
80
  mountedRef.current = false;
64
81
  if (resetTimeoutRef.current) {
65
82
  clearTimeout(resetTimeoutRef.current);
83
+ console.log("โน๏ธ Cleared reset timeout");
66
84
  }
85
+ clearNotification();
67
86
  };
68
87
  }, []);
69
88
 
70
89
  useEffect(() => {
71
90
  dataRef.current = data;
91
+ console.log(
92
+ "๐Ÿ“‹ Updated dataRef with new data:",
93
+ data ? "Available" : "Empty"
94
+ );
72
95
  }, [data]);
73
96
 
97
+ const animateIcon = useCallback(() => {
98
+ // Reset animation
99
+ iconScaleAnim.setValue(1);
100
+ iconOpacityAnim.setValue(0);
101
+
102
+ // Start animation sequence
103
+ Animated.sequence([
104
+ Animated.parallel([
105
+ Animated.timing(iconOpacityAnim, {
106
+ toValue: 1,
107
+ duration: 300,
108
+ useNativeDriver: true,
109
+ }),
110
+ Animated.spring(iconScaleAnim, {
111
+ toValue: 1.2,
112
+ friction: 3,
113
+ useNativeDriver: true,
114
+ }),
115
+ ]),
116
+ Animated.spring(iconScaleAnim, {
117
+ toValue: 1,
118
+ friction: 5,
119
+ useNativeDriver: true,
120
+ }),
121
+ ]).start();
122
+ }, [iconScaleAnim, iconOpacityAnim]);
123
+
74
124
  const updateState = useCallback((newState) => {
75
125
  if (mountedRef.current) {
76
126
  setState((prev) => {
77
127
  const merged = { ...prev, ...newState };
78
- return prev !== merged ? merged : prev;
128
+ if (JSON.stringify(prev) !== JSON.stringify(merged)) {
129
+ console.log("๐Ÿ”„ State updated:", merged);
130
+
131
+ // Animate icon when step changes
132
+ if (newState.currentStep && newState.currentStep !== prev.currentStep) {
133
+ animateIcon();
134
+ }
135
+
136
+ return merged;
137
+ }
138
+ return prev;
79
139
  });
140
+ } else {
141
+ console.log("๐Ÿšซ State update skipped - component unmounted");
80
142
  }
81
- }, []);
143
+ }, [animateIcon]);
82
144
 
83
145
  const resetState = useCallback(() => {
84
- console.log("๐Ÿ”„ Resetting biometric modal...");
146
+ console.log("๐Ÿ”„ Resetting biometric modal state");
147
+ onclose(false);
85
148
  setState({
86
149
  isLoading: false,
150
+ loadingType: LOADING_TYPES.NONE,
87
151
  currentStep: "Start",
88
152
  employeeData: null,
89
153
  animationState: ANIMATION_STATES.FACE_SCAN,
@@ -91,112 +155,152 @@ const BiometricVerificationModal = React.memo(
91
155
  setModalVisible(false);
92
156
  processedRef.current = false;
93
157
  resetCountdown();
94
-
158
+ clearNotification();
159
+
95
160
  if (resetTimeoutRef.current) {
96
161
  clearTimeout(resetTimeoutRef.current);
97
162
  resetTimeoutRef.current = null;
163
+ console.log("โน๏ธ Cleared reset timeout during reset");
98
164
  }
99
- }, [resetCountdown]);
165
+ }, [resetCountdown, clearNotification]);
100
166
 
101
167
  const handleProcessError = useCallback(
102
168
  (message, errorObj = null) => {
103
- if (errorObj) console.error(message, errorObj);
169
+ console.error("โŒ Process Error:", message, errorObj || "");
170
+ if (errorObj) console.error("Error details:", errorObj);
171
+
104
172
  notifyMessage(message, "error");
105
173
  updateState({
106
174
  animationState: ANIMATION_STATES.ERROR,
107
175
  isLoading: false,
176
+ loadingType: LOADING_TYPES.NONE,
108
177
  });
109
-
178
+
110
179
  if (resetTimeoutRef.current) {
111
180
  clearTimeout(resetTimeoutRef.current);
112
181
  }
113
- resetTimeoutRef.current = setTimeout(resetState, 1200);
182
+ resetTimeoutRef.current = setTimeout(() => {
183
+ console.log("โฐ Error timeout completed - resetting state");
184
+ resetState();
185
+ }, 1200);
114
186
  },
115
187
  [notifyMessage, resetState, updateState]
116
188
  );
117
189
 
118
190
  const handleCountdownFinish = useCallback(() => {
191
+ console.log("โฐ Countdown finished");
119
192
  handleProcessError("Time is up! Please try again.");
120
- if (navigation.canGoBack()) navigation.goBack();
193
+ if (navigation.canGoBack()) {
194
+ console.log("โ†ฉ๏ธ Navigating back due to timeout");
195
+ navigation.goBack();
196
+ }
121
197
  }, [handleProcessError, navigation]);
122
198
 
123
199
  const validateApiUrl = useCallback(() => {
124
200
  if (!apiurl || typeof apiurl !== "string") {
201
+ console.error("โŒ Invalid API URL:", apiurl);
125
202
  handleProcessError("Invalid API URL configuration.");
126
203
  return false;
127
204
  }
205
+ console.log("โœ… API URL validated:", apiurl);
128
206
  return true;
129
207
  }, [apiurl, handleProcessError]);
130
208
 
131
209
  const uploadFaceScan = useCallback(
132
210
  async (selfie) => {
211
+ console.log("๐Ÿ“ธ Uploading face scan");
212
+
133
213
  if (!validateApiUrl()) return;
134
214
  const currentData = dataRef.current;
215
+
135
216
  if (!currentData) {
217
+ console.error("โŒ No employee data available");
136
218
  handleProcessError("Employee data not found.");
137
219
  return;
138
220
  }
139
-
221
+
140
222
  updateState({
141
223
  isLoading: true,
224
+ loadingType: LOADING_TYPES.FACE_RECOGNITION,
142
225
  animationState: ANIMATION_STATES.PROCESSING,
143
226
  });
144
-
227
+
145
228
  InteractionManager.runAfterInteractions(async () => {
146
229
  let base64;
147
230
  try {
231
+ console.log("๐Ÿ–ผ๏ธ Converting image to base64");
232
+ updateState({
233
+ loadingType: LOADING_TYPES.IMAGE_PROCESSING,
234
+ });
235
+
148
236
  base64 = await convertImageToBase64(selfie?.uri);
237
+ console.log("โœ… Image converted successfully");
149
238
  } catch (err) {
239
+ console.error("โŒ Image conversion failed:", err);
150
240
  handleProcessError("Image conversion failed.", err);
151
241
  return;
152
242
  }
153
-
243
+
154
244
  if (!base64) {
245
+ console.error("โŒ Empty base64 data");
155
246
  handleProcessError("Failed to process image.");
156
247
  return;
157
248
  }
158
-
249
+
159
250
  try {
160
251
  const body = { image: base64 };
161
252
  const header = { faceid: currentData };
162
253
  const buttonapi = `${apiurl}python/recognize`;
163
- console.log("buttonapi", buttonapi);
164
-
254
+ console.log("๐ŸŒ Calling face recognition API:", buttonapi);
255
+
256
+ updateState({
257
+ loadingType: LOADING_TYPES.NETWORK_REQUEST,
258
+ });
259
+
165
260
  const response = await networkServiceCall(
166
261
  "POST",
167
262
  buttonapi,
168
263
  header,
169
264
  body
170
265
  );
171
-
266
+
267
+ console.log("๐Ÿ“จ API Response:", response);
268
+
172
269
  if (response?.httpstatus === 200) {
270
+ console.log("โœ… Face recognition successful");
173
271
  responseRef.current = response;
174
272
  updateState({
175
273
  employeeData: response.data?.data || null,
176
274
  animationState: ANIMATION_STATES.SUCCESS,
177
275
  isLoading: false,
276
+ loadingType: LOADING_TYPES.NONE,
178
277
  });
179
-
278
+
180
279
  notifyMessage("Identity verified successfully!", "success");
181
-
280
+
182
281
  if (qrscan) {
282
+ console.log("๐Ÿ”œ Proceeding to QR code scan");
183
283
  setTimeout(() => startQRCodeScan(), 1200);
184
284
  } else {
285
+ console.log("โœ… Verification complete - calling callback");
185
286
  safeCallback(responseRef.current);
186
287
  if (resetTimeoutRef.current) {
187
288
  clearTimeout(resetTimeoutRef.current);
188
289
  }
189
290
  resetTimeoutRef.current = setTimeout(() => {
291
+ console.log("โฐ Success timeout completed - resetting");
190
292
  resetState();
191
293
  }, 1200);
192
294
  }
193
295
  } else {
296
+ console.warn("โš ๏ธ Face recognition failed:", response?.data?.error);
194
297
  handleProcessError(
195
298
  response?.data?.error?.message ||
196
299
  "Face not recognized. Please try again."
197
300
  );
198
301
  }
199
302
  } catch (error) {
303
+ console.error("โŒ Network request failed:", error);
200
304
  handleProcessError(
201
305
  "Connection error. Please check your network.",
202
306
  error
@@ -212,99 +316,126 @@ const BiometricVerificationModal = React.memo(
212
316
  updateState,
213
317
  validateApiUrl,
214
318
  safeCallback,
319
+ handleProcessError
215
320
  ]
216
321
  );
217
322
 
218
- const handleStartFaceScan = useCallback(() => {
219
- updateState({
220
- currentStep: "Identity Verification",
221
- animationState: ANIMATION_STATES.FACE_SCAN,
222
- });
223
-
224
- navigation.navigate("CCaptureImageWithoutEdit", {
225
- facedetection: true,
226
- cameratype: "front",
227
- onSelect: uploadFaceScan,
228
- });
229
- }, [navigation, updateState, uploadFaceScan]);
230
-
231
- const startQRCodeScan = useCallback(() => {
232
- updateState({
233
- currentStep: "Location Verification",
234
- animationState: ANIMATION_STATES.QR_SCAN,
235
- });
236
-
237
- navigation.navigate("CCaptureImageWithoutEdit", {
238
- hidebuttons: true,
239
- cameratype: "back",
240
- cameramoduletype: 2,
241
- onSelect: handleQRScanned,
242
- });
243
- }, [navigation, updateState, handleQRScanned]);
244
-
245
323
  const handleQRScanned = useCallback(
246
324
  async (qrCodeData) => {
325
+ console.log("๐Ÿ” Processing scanned QR code");
326
+
247
327
  if (!validateApiUrl()) return;
248
-
328
+
249
329
  updateState({
250
330
  animationState: ANIMATION_STATES.PROCESSING,
251
331
  isLoading: true,
332
+ loadingType: LOADING_TYPES.LOCATION_VERIFICATION,
252
333
  });
253
-
334
+
254
335
  try {
336
+ console.log("๐Ÿ“ Requesting location permission");
337
+ updateState({
338
+ loadingType: LOADING_TYPES.LOCATION_PERMISSION,
339
+ });
340
+
255
341
  const hasPermission = await requestLocationPermission();
342
+
256
343
  if (!hasPermission) {
344
+ console.error("โŒ Location permission denied");
257
345
  handleProcessError("Location permission not granted.");
258
346
  return;
259
347
  }
260
-
348
+
349
+ console.log("โœ… Location permission granted");
350
+
261
351
  const qrString =
262
352
  typeof qrCodeData === "object" ? qrCodeData?.data : qrCodeData;
263
-
353
+
264
354
  if (!qrString || typeof qrString !== "string") {
355
+ console.error("โŒ Invalid QR code data:", qrCodeData);
265
356
  handleProcessError("Invalid QR code. Please try again.");
266
357
  return;
267
358
  }
268
-
359
+
360
+ console.log("๐Ÿ“‹ QR code content:", qrString);
361
+
362
+ updateState({
363
+ loadingType: LOADING_TYPES.GETTING_LOCATION,
364
+ });
365
+
269
366
  const location = await getCurrentLocation();
367
+ console.log("๐Ÿ“ Device location:", location);
368
+
270
369
  const [latStr, lngStr] = qrString.split(",");
271
370
  const lat = parseFloat(latStr);
272
371
  const lng = parseFloat(lngStr);
273
372
  const validCoords = !isNaN(lat) && !isNaN(lng);
274
373
  const validDev =
275
374
  !isNaN(location?.latitude) && !isNaN(location?.longitude);
276
-
375
+
376
+ console.log(
377
+ "๐Ÿ“Š Coordinates validation - QR:",
378
+ validCoords,
379
+ "Device:",
380
+ validDev
381
+ );
382
+
277
383
  if (validCoords && validDev) {
384
+ updateState({
385
+ loadingType: LOADING_TYPES.CALCULATING_DISTANCE,
386
+ });
387
+
278
388
  const distance = getDistanceInMeters(
279
389
  lat,
280
390
  lng,
281
391
  location.latitude,
282
392
  location.longitude
283
393
  );
284
-
394
+
395
+ console.log(
396
+ "๐Ÿ“ Distance calculated:",
397
+ distance.toFixed(0),
398
+ "meters"
399
+ );
400
+
285
401
  if (distance <= MAX_DISTANCE_METERS) {
402
+ console.log("โœ… Location verified successfully");
286
403
  safeCallback(responseRef.current);
287
404
  notifyMessage("Location verified successfully!", "success");
288
405
  updateState({
289
406
  animationState: ANIMATION_STATES.SUCCESS,
290
407
  isLoading: false,
408
+ loadingType: LOADING_TYPES.NONE,
291
409
  });
292
-
410
+
293
411
  if (resetTimeoutRef.current) {
294
412
  clearTimeout(resetTimeoutRef.current);
295
413
  }
296
414
  resetTimeoutRef.current = setTimeout(() => {
415
+ console.log("โฐ Location success timeout - resetting");
297
416
  resetState();
298
417
  }, 1200);
299
418
  } else {
419
+ console.warn(
420
+ "โš ๏ธ Location mismatch:",
421
+ distance.toFixed(0),
422
+ "m away"
423
+ );
300
424
  handleProcessError(
301
425
  `Location mismatch (${distance.toFixed(0)}m away).`
302
426
  );
303
427
  }
304
428
  } else {
429
+ console.error("โŒ Invalid coordinates:", {
430
+ qrLat: lat,
431
+ qrLng: lng,
432
+ devLat: location.latitude,
433
+ devLng: location.longitude,
434
+ });
305
435
  handleProcessError("Invalid coordinates in QR code.");
306
436
  }
307
437
  } catch (error) {
438
+ console.error("โŒ Location verification failed:", error);
308
439
  handleProcessError(
309
440
  "Unable to verify location. Please try again.",
310
441
  error
@@ -319,17 +450,62 @@ const BiometricVerificationModal = React.memo(
319
450
  updateState,
320
451
  validateApiUrl,
321
452
  safeCallback,
453
+ handleProcessError
322
454
  ]
323
455
  );
324
456
 
457
+ const handleImageCapture = useCallback(
458
+ async (capturedData) => {
459
+ console.log("๐Ÿ“ท Image captured for step:", state.currentStep);
460
+
461
+ if (state.currentStep === "Identity Verification") {
462
+ console.log("๐Ÿ‘ค Processing face verification");
463
+ uploadFaceScan(capturedData);
464
+ } else if (state.currentStep === "Location Verification") {
465
+ console.log("๐Ÿ“ Processing QR code verification");
466
+ handleQRScanned(capturedData);
467
+ }
468
+ },
469
+ [state.currentStep, uploadFaceScan, handleQRScanned]
470
+ );
471
+
472
+ const handleStartFaceScan = useCallback(() => {
473
+ console.log("๐Ÿ‘ค Starting face scan");
474
+ updateState({
475
+ currentStep: "Identity Verification",
476
+ animationState: ANIMATION_STATES.FACE_SCAN,
477
+ });
478
+ setCameraType("front");
479
+ }, [updateState]);
480
+
481
+ const startQRCodeScan = useCallback(() => {
482
+ console.log("๐Ÿ“ Starting QR code scan");
483
+ updateState({
484
+ currentStep: "Location Verification",
485
+ animationState: ANIMATION_STATES.QR_SCAN,
486
+ });
487
+ setCameraType("back");
488
+ }, [updateState]);
489
+
490
+ const toggleCamera = useCallback(() => {
491
+ console.log("๐Ÿ”„ Toggling camera");
492
+ setCameraType((prevType) => {
493
+ const newType = prevType === "front" ? "back" : "front";
494
+ console.log("๐Ÿ“ท Switching to camera:", newType);
495
+ return newType;
496
+ });
497
+ }, []);
498
+
325
499
  const startProcess = useCallback(() => {
500
+ console.log("๐Ÿš€ Starting verification process");
326
501
  startCountdown(COUNTDOWN_DURATION, handleCountdownFinish);
327
502
  handleStartFaceScan();
328
503
  }, [handleCountdownFinish, handleStartFaceScan, startCountdown]);
329
504
 
330
505
  useEffect(() => {
331
- if (data && !modalVisible) {
332
- console.log("๐Ÿ“ฅ New donor data received:", data);
506
+ if (data && !modalVisible && !processedRef.current) {
507
+ console.log("๐Ÿ“ฅ New data received, opening modal");
508
+ processedRef.current = true;
333
509
  setModalVisible(true);
334
510
  startProcess();
335
511
  }
@@ -342,6 +518,12 @@ const BiometricVerificationModal = React.memo(
342
518
  [state.isLoading, state.animationState, state.currentStep, apiurl]
343
519
  );
344
520
 
521
+ const shouldShowCamera =
522
+ (state.currentStep === "Identity Verification" ||
523
+ state.currentStep === "Location Verification") &&
524
+ state.animationState !== ANIMATION_STATES.SUCCESS &&
525
+ state.animationState !== ANIMATION_STATES.ERROR;
526
+
345
527
  return (
346
528
  <>
347
529
  <Modal
@@ -351,41 +533,60 @@ const BiometricVerificationModal = React.memo(
351
533
  onRequestClose={resetState}
352
534
  statusBarTranslucent
353
535
  >
354
- <View style={styles.modalBg}>
536
+ <View style={styles.modalContainer}>
355
537
  <TouchableOpacity
356
- style={styles.close}
538
+ style={styles.closeButton}
357
539
  onPress={resetState}
358
540
  accessibilityLabel="Close modal"
359
541
  hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
360
542
  >
361
543
  <Icon name="close" size={24} color={COLORS.light} />
362
544
  </TouchableOpacity>
363
-
364
- <Text style={styles.title}>Biometric Verification</Text>
365
- <Text style={styles.subTitle}>{state.currentStep}</Text>
366
-
367
- {state.employeeData && (
368
- <EmployeeCard
369
- employeeData={state.employeeData}
370
- apiurl={apiurl}
545
+
546
+ <View style={styles.headerContainer}>
547
+ <StepIcon
548
+ currentStep={state.currentStep}
549
+ animationState={state.animationState}
550
+ scaleAnim={iconScaleAnim}
551
+ opacityAnim={iconOpacityAnim}
552
+ />
553
+
554
+ <View style={styles.titleContainer}>
555
+ <Text style={styles.title}>Biometric Verification</Text>
556
+ <Text style={styles.subtitle}>{state.currentStep}</Text>
557
+ </View>
558
+ </View>
559
+
560
+ <StepIndicator currentStep={state.currentStep} />
561
+
562
+ {shouldShowCamera && !state.isLoading && (
563
+ <CaptureImageWithoutEdit
564
+ cameraType={cameraType}
565
+ onCapture={handleImageCapture}
566
+ onToggleCamera={toggleCamera}
567
+ showCodeScanner={state.currentStep === "Location Verification"}
568
+ isLoading={state.isLoading}
569
+ currentStep={state.currentStep}
371
570
  />
372
571
  )}
373
-
572
+
573
+ {state.employeeData && (
574
+ <CCard employeeData={state.employeeData} apiurl={apiurl} />
575
+ )}
576
+
374
577
  <Notification
375
578
  notification={notification}
376
579
  fadeAnim={fadeAnim}
377
580
  slideAnim={slideAnim}
378
581
  />
379
-
582
+
380
583
  <CountdownTimer
381
584
  duration={COUNTDOWN_DURATION}
382
585
  currentTime={countdown}
383
586
  />
384
-
385
- <Loader
386
- state={state.animationState}
387
- source={loaderSource}
388
- isLoading={state.isLoading}
587
+ <Loader
588
+ gifSource={loaderSource}
589
+ visible={state.isLoading}
389
590
  />
390
591
  </View>
391
592
  </Modal>
@@ -394,4 +595,54 @@ const BiometricVerificationModal = React.memo(
394
595
  }
395
596
  );
396
597
 
598
+ const styles = StyleSheet.create({
599
+ modalContainer: {
600
+ flex: 1,
601
+ backgroundColor: 'rgba(0, 0, 0, 0.85)',
602
+ justifyContent: 'center',
603
+ alignItems: 'center',
604
+ paddingVertical: 50,
605
+ },
606
+ headerContainer: {
607
+ flexDirection: 'row',
608
+ alignItems: 'center',
609
+ marginBottom: 15,
610
+ },
611
+ titleContainer: {
612
+ flex: 1,
613
+ },
614
+ title: {
615
+ fontSize: 26,
616
+ fontWeight: '700',
617
+ color: COLORS.light,
618
+ marginBottom: 5,
619
+ textAlign: 'left',
620
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif',
621
+ textShadowColor: 'rgba(0, 0, 0, 0.3)',
622
+ textShadowOffset: { width: 1, height: 1 },
623
+ textShadowRadius: 3,
624
+ },
625
+ subtitle: {
626
+ fontSize: 18,
627
+ color: COLORS.light,
628
+ marginBottom: 0,
629
+ fontWeight: '600',
630
+ textAlign: 'left',
631
+ opacity: 0.9,
632
+ fontFamily: Platform.OS === 'ios' ? 'Helvetica Neue' : 'sans-serif-medium',
633
+ textShadowColor: 'rgba(0, 0, 0, 0.2)',
634
+ textShadowOffset: { width: 1, height: 1 },
635
+ textShadowRadius: 2,
636
+ },
637
+ closeButton: {
638
+ position: 'absolute',
639
+ top: Platform.OS === 'ios' ? 50 : 30,
640
+ right: 20,
641
+ zIndex: 10,
642
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
643
+ borderRadius: 20,
644
+ padding: 8,
645
+ },
646
+ });
647
+
397
648
  export default BiometricVerificationModal;