omnipay-reactnative-sdk 1.1.9 → 1.2.1-beta.0

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.
Files changed (40) hide show
  1. package/README.md +153 -31
  2. package/lib/commonjs/components/FaceVerification.js +755 -0
  3. package/lib/commonjs/components/FaceVerification.js.map +1 -0
  4. package/lib/commonjs/components/OmnipayProvider.js +65 -4
  5. package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
  6. package/lib/commonjs/types/faceVerification.js +2 -0
  7. package/lib/commonjs/types/faceVerification.js.map +1 -0
  8. package/lib/commonjs/types/index.js +17 -0
  9. package/lib/commonjs/types/index.js.map +1 -0
  10. package/lib/module/components/FaceVerification.js +746 -0
  11. package/lib/module/components/FaceVerification.js.map +1 -0
  12. package/lib/module/components/OmnipayProvider.js +65 -4
  13. package/lib/module/components/OmnipayProvider.js.map +1 -1
  14. package/lib/module/types/faceVerification.js +2 -0
  15. package/lib/module/types/faceVerification.js.map +1 -0
  16. package/lib/module/types/index.js +2 -0
  17. package/lib/module/types/index.js.map +1 -0
  18. package/lib/typescript/components/FaceVerification.d.ts +10 -0
  19. package/lib/typescript/components/FaceVerification.d.ts.map +1 -0
  20. package/lib/typescript/components/OmnipayProvider.d.ts +5 -3
  21. package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
  22. package/lib/typescript/components/OmnipayView.d.ts +2 -0
  23. package/lib/typescript/components/OmnipayView.d.ts.map +1 -1
  24. package/lib/typescript/components/views/BvnVerification.d.ts +2 -0
  25. package/lib/typescript/components/views/BvnVerification.d.ts.map +1 -1
  26. package/lib/typescript/components/views/PaylaterAgreement.d.ts +2 -0
  27. package/lib/typescript/components/views/PaylaterAgreement.d.ts.map +1 -1
  28. package/lib/typescript/components/views/Registration.d.ts +2 -0
  29. package/lib/typescript/components/views/Registration.d.ts.map +1 -1
  30. package/lib/typescript/hooks/useOmnipay.d.ts +5 -3
  31. package/lib/typescript/hooks/useOmnipay.d.ts.map +1 -1
  32. package/lib/typescript/types/faceVerification.d.ts +18 -0
  33. package/lib/typescript/types/faceVerification.d.ts.map +1 -0
  34. package/lib/typescript/types/index.d.ts +2 -0
  35. package/lib/typescript/types/index.d.ts.map +1 -0
  36. package/package.json +10 -4
  37. package/src/components/FaceVerification.tsx +884 -0
  38. package/src/components/OmnipayProvider.tsx +81 -4
  39. package/src/types/faceVerification.ts +27 -0
  40. package/src/types/index.ts +1 -0
@@ -0,0 +1,884 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ TouchableOpacity,
7
+ Animated,
8
+ Image,
9
+ Linking,
10
+ Platform,
11
+ } from 'react-native';
12
+ import {
13
+ useCameraDevice,
14
+ useCameraPermission,
15
+ Frame,
16
+ useCameraFormat,
17
+ } from 'react-native-vision-camera';
18
+ import {
19
+ Face,
20
+ Camera,
21
+ FaceDetectionOptions,
22
+ } from 'react-native-vision-camera-face-detector';
23
+ import Svg, { Circle } from 'react-native-svg';
24
+
25
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
26
+
27
+ type VerificationStep =
28
+ | 'position_face'
29
+ | 'blink'
30
+ | 'smile'
31
+ | 'final_position'
32
+ | 'complete';
33
+
34
+ interface FaceVerificationProps {
35
+ onSuccess?: (capturedImageBase64: string) => void;
36
+ onFailure?: () => void;
37
+ onCancel?: () => void;
38
+ onImageCaptured?: (livenessImage: string) => void;
39
+ }
40
+
41
+ const FaceVerification: React.FC<FaceVerificationProps> = ({
42
+ onSuccess,
43
+ onFailure,
44
+ onCancel,
45
+ onImageCaptured,
46
+ }) => {
47
+ const device = useCameraDevice('front');
48
+ const faceDetectionOptions = useRef<FaceDetectionOptions>({
49
+ landmarkMode: 'all',
50
+ classificationMode: 'all',
51
+ }).current;
52
+
53
+ const photoFormat = useCameraFormat(device, [
54
+ { photoResolution: { width: 1280, height: 720 } },
55
+ ]);
56
+
57
+ const { hasPermission, requestPermission } = useCameraPermission();
58
+
59
+ const spinValue = useRef(new Animated.Value(0)).current;
60
+ const progressValue = useRef(new Animated.Value(955)).current; // 955 = circle circumference for r=152
61
+
62
+ const [isCameraActive, setIsCameraActive] = useState(false);
63
+ const [isLoading, setIsLoading] = useState(false);
64
+ const [permissionDenied, setPermissionDenied] = useState(false);
65
+ const [isInitializingCamera, setIsInitializingCamera] = useState(true);
66
+
67
+ const [currentStep, setCurrentStep] =
68
+ useState<VerificationStep>('position_face');
69
+ const [isCapturing, setIsCapturing] = useState(false);
70
+ const [capturedImage, setCapturedImage] = useState<string | null>(null);
71
+
72
+ const frameCountRef = useRef(0);
73
+ const finalPositionFrameCountRef = useRef(0);
74
+ const faceHistoryRef = useRef<Face[]>([]);
75
+ const cameraRef = useRef<any>(null);
76
+
77
+ // RAF optimization: buffer latest face data and process on animation frames
78
+ const latestFaceDataRef = useRef<{ faces: Face[]; frame: Frame } | null>(
79
+ null
80
+ );
81
+ const rafIdRef = useRef<number | null>(null);
82
+ const isProcessingRef = useRef(false);
83
+
84
+ // Handle camera device initialization and auto-start
85
+ useEffect(() => {
86
+ if (device !== undefined) {
87
+ setIsInitializingCamera(false);
88
+
89
+ // If permissions are already granted, start camera immediately
90
+ if (
91
+ hasPermission &&
92
+ !isCameraActive &&
93
+ !permissionDenied &&
94
+ !capturedImage
95
+ ) {
96
+ setIsCameraActive(true);
97
+ progressValue.setValue(955);
98
+ }
99
+ }
100
+ }, [
101
+ device,
102
+ hasPermission,
103
+ isCameraActive,
104
+ permissionDenied,
105
+ capturedImage,
106
+ progressValue,
107
+ ]);
108
+
109
+ // Start loading animation during camera initialization
110
+ useEffect(() => {
111
+ if (isInitializingCamera) {
112
+ setIsLoading(true);
113
+ } else {
114
+ setIsLoading(false);
115
+ }
116
+ }, [isInitializingCamera]);
117
+
118
+ useEffect(() => {
119
+ const spin = Animated.loop(
120
+ Animated.timing(spinValue, {
121
+ toValue: 1,
122
+ duration: 1000,
123
+ useNativeDriver: true,
124
+ })
125
+ );
126
+
127
+ if (isLoading) {
128
+ spin.start();
129
+ } else {
130
+ spin.stop();
131
+ spinValue.setValue(0);
132
+ }
133
+
134
+ return () => spin.stop();
135
+ }, [isLoading, spinValue]);
136
+
137
+ useEffect(() => {
138
+ const progressPercentage = calculateProgress();
139
+ const strokeDasharray = 955;
140
+ const targetOffset =
141
+ strokeDasharray - (progressPercentage / 100) * strokeDasharray;
142
+
143
+ if (isCameraActive && progressPercentage >= 0) {
144
+ Animated.timing(progressValue, {
145
+ toValue: targetOffset,
146
+ duration: 800,
147
+ useNativeDriver: false,
148
+ }).start();
149
+ }
150
+ }, [currentStep, progressValue, isCameraActive]);
151
+
152
+ // RAF-based face processing to throttle heavy computations
153
+ const processFaceData = () => {
154
+ if (!latestFaceDataRef.current || isProcessingRef.current) {
155
+ rafIdRef.current = null;
156
+ return;
157
+ }
158
+
159
+ isProcessingRef.current = true;
160
+ const { faces } = latestFaceDataRef.current;
161
+
162
+ // Clear the buffer since we're processing this data
163
+ latestFaceDataRef.current = null;
164
+
165
+ try {
166
+ handleFaceDetectionLogic(faces);
167
+ } finally {
168
+ isProcessingRef.current = false;
169
+ rafIdRef.current = null;
170
+ }
171
+ };
172
+
173
+ // Cleanup RAF on unmount
174
+ useEffect(() => {
175
+ return () => {
176
+ if (rafIdRef.current) {
177
+ cancelAnimationFrame(rafIdRef.current);
178
+ }
179
+ };
180
+ }, []);
181
+
182
+ const getInstructionText = () => {
183
+ switch (currentStep) {
184
+ case 'position_face':
185
+ return 'Position your face in the frame';
186
+ case 'blink':
187
+ return 'Please blink your eyes';
188
+ case 'smile':
189
+ return 'Now smile';
190
+ case 'final_position':
191
+ return 'Look straight at the camera and hold still';
192
+ case 'complete':
193
+ return '';
194
+ default:
195
+ return 'Position your face in the frame';
196
+ }
197
+ };
198
+
199
+ const calculateProgress = () => {
200
+ switch (currentStep) {
201
+ case 'position_face':
202
+ return 10;
203
+ case 'blink':
204
+ return 30;
205
+ case 'smile':
206
+ return 60;
207
+ case 'final_position':
208
+ return 80;
209
+ case 'complete':
210
+ return 100;
211
+ default:
212
+ return 0;
213
+ }
214
+ };
215
+
216
+ const detectBlink = (): boolean => {
217
+ if (frameCountRef.current < 20 || faceHistoryRef.current.length < 4)
218
+ return false;
219
+
220
+ const threshold = {
221
+ eyeOpen: 0.5,
222
+ eyeClosed: 0.3,
223
+ };
224
+
225
+ const recentFrames = faceHistoryRef.current.slice(-4);
226
+
227
+ for (let i = 0; i < recentFrames.length - 1; i++) {
228
+ const frame1 = recentFrames[i];
229
+ const frame2 = recentFrames[i + 1];
230
+
231
+ if (!frame1 || !frame2) continue;
232
+
233
+ const eyesWereOpen =
234
+ frame1.leftEyeOpenProbability > threshold.eyeOpen &&
235
+ frame1.rightEyeOpenProbability > threshold.eyeOpen;
236
+
237
+ const eyesAreClosed =
238
+ frame2.leftEyeOpenProbability < threshold.eyeClosed &&
239
+ frame2.rightEyeOpenProbability < threshold.eyeClosed;
240
+
241
+ if (eyesWereOpen && eyesAreClosed) {
242
+ return true;
243
+ }
244
+ }
245
+
246
+ return false;
247
+ };
248
+
249
+ const detectSmile = (face: Face): boolean => {
250
+ if (frameCountRef.current < 20 || faceHistoryRef.current.length < 3)
251
+ return false;
252
+
253
+ const threshold = 0.7;
254
+ const currentSmiling = face.smilingProbability > threshold;
255
+
256
+ const recentFrames = faceHistoryRef.current.slice(-3);
257
+ const hadNotSmilingFrames = recentFrames.some(
258
+ (frame: Face) => frame.smilingProbability < 0.4
259
+ );
260
+
261
+ return currentSmiling && hadNotSmilingFrames;
262
+ };
263
+
264
+ const isInFinalPosition = (face: Face): boolean => {
265
+ const isLookingStraight = Math.abs(face.yawAngle) < 15;
266
+ const eyesOpen =
267
+ face.leftEyeOpenProbability > 0.5 && face.rightEyeOpenProbability > 0.5;
268
+ const isNotSmiling = face.smilingProbability < 0.1;
269
+
270
+ return isLookingStraight && eyesOpen && isNotSmiling;
271
+ };
272
+
273
+ // Fast callback that just buffers data - called by camera at high frequency
274
+ function handleFacesDetection(faces: Face[], frame: Frame) {
275
+ // Store the latest face data
276
+ latestFaceDataRef.current = { faces, frame };
277
+
278
+ // Schedule processing on next animation frame if not already scheduled
279
+ if (!rafIdRef.current) {
280
+ rafIdRef.current = requestAnimationFrame(processFaceData);
281
+ }
282
+ }
283
+
284
+ // Heavy processing logic - called at animation frame rate (60fps max)
285
+ function handleFaceDetectionLogic(faces: Face[]) {
286
+ if (faces.length === 0) {
287
+ if (currentStep !== 'position_face' && currentStep !== 'complete') {
288
+ setCurrentStep('position_face');
289
+ frameCountRef.current = 0;
290
+ faceHistoryRef.current = [];
291
+ finalPositionFrameCountRef.current = 0;
292
+ }
293
+ return;
294
+ }
295
+
296
+ const face = faces[0];
297
+ if (!face) return;
298
+
299
+ frameCountRef.current++;
300
+
301
+ faceHistoryRef.current = [...faceHistoryRef.current.slice(-7), face];
302
+
303
+ switch (currentStep) {
304
+ case 'position_face':
305
+ if (frameCountRef.current > 10) {
306
+ setCurrentStep('blink');
307
+ }
308
+ break;
309
+
310
+ case 'blink':
311
+ if (detectBlink()) {
312
+ setCurrentStep('smile');
313
+ }
314
+ break;
315
+
316
+ case 'smile':
317
+ if (detectSmile(face)) {
318
+ setCurrentStep('final_position');
319
+ }
320
+ break;
321
+
322
+ case 'final_position':
323
+ if (isInFinalPosition(face)) {
324
+ finalPositionFrameCountRef.current++;
325
+ // Require 5 consecutive frames for stability before capturing
326
+ if (finalPositionFrameCountRef.current >= 5) {
327
+ setCurrentStep('complete');
328
+ captureCurrentFrame();
329
+ }
330
+ } else {
331
+ finalPositionFrameCountRef.current = 0;
332
+ }
333
+ break;
334
+
335
+ default:
336
+ break;
337
+ }
338
+ }
339
+
340
+ const captureCurrentFrame = async () => {
341
+ if (isCapturing || !cameraRef.current) return;
342
+
343
+ try {
344
+ setIsCapturing(true);
345
+
346
+ const photo = await cameraRef.current.takePhoto({
347
+ enableShutterSound: false,
348
+ });
349
+
350
+ const response = await fetch(`file://${photo.path}`);
351
+ const blob = await response.blob();
352
+
353
+ const reader = new FileReader();
354
+ reader.onloadend = () => {
355
+ const base64String = reader.result as string;
356
+ setCapturedImage(base64String);
357
+ setIsCameraActive(false);
358
+ };
359
+ reader.readAsDataURL(blob);
360
+ } catch (error) {
361
+ onFailure?.();
362
+ } finally {
363
+ setIsCapturing(false);
364
+ }
365
+ };
366
+
367
+ const handleContinue = () => {
368
+ if (capturedImage) {
369
+ const base64String = capturedImage.replace(
370
+ /^data:image\/[a-z]+;base64,/,
371
+ ''
372
+ );
373
+
374
+ // Call onImageCaptured immediately for UI feedback
375
+ onImageCaptured?.(base64String);
376
+
377
+ // Still call onSuccess for backward compatibility
378
+ onSuccess?.(base64String);
379
+ }
380
+ };
381
+
382
+ const handleRetake = () => {
383
+ setCurrentStep('position_face');
384
+ faceHistoryRef.current = [];
385
+ frameCountRef.current = 0;
386
+ setCapturedImage(null);
387
+ finalPositionFrameCountRef.current = 0;
388
+ progressValue.setValue(955);
389
+
390
+ setIsCameraActive(true);
391
+ setIsLoading(true);
392
+
393
+ setTimeout(() => {
394
+ setIsLoading(false);
395
+ }, 1000);
396
+ };
397
+
398
+ const startCamera = async () => {
399
+ setIsLoading(true);
400
+ setPermissionDenied(false);
401
+
402
+ try {
403
+ const permission = await requestPermission();
404
+
405
+ if (permission === true) {
406
+ setIsCameraActive(true);
407
+ progressValue.setValue(955);
408
+
409
+ setTimeout(() => {
410
+ setIsLoading(false);
411
+ }, 1000);
412
+ } else {
413
+ setPermissionDenied(true);
414
+ setIsLoading(false);
415
+ }
416
+ } catch (error) {
417
+ setPermissionDenied(true);
418
+ setIsLoading(false);
419
+ }
420
+ };
421
+
422
+ const openSettings = () => {
423
+ if (Platform.OS === 'ios') {
424
+ Linking.openURL('app-settings:');
425
+ } else {
426
+ Linking.openSettings();
427
+ }
428
+ };
429
+
430
+ if (permissionDenied) {
431
+ return (
432
+ <View style={[styles.container]}>
433
+ <View style={styles.centeredContainer}>
434
+ <View style={styles.permissionDeniedContainer}>
435
+ <Text style={styles.permissionDeniedTitle}>
436
+ Camera Access Required
437
+ </Text>
438
+ <Text style={styles.permissionDeniedText}>
439
+ This app needs camera access to verify your identity. Please allow
440
+ camera permission in your device settings.
441
+ </Text>
442
+
443
+ <View style={styles.permissionActions}>
444
+ <TouchableOpacity
445
+ style={styles.settingsButton}
446
+ onPress={openSettings}
447
+ >
448
+ <Text style={styles.settingsButtonText}>Open Settings</Text>
449
+ </TouchableOpacity>
450
+
451
+ <TouchableOpacity
452
+ style={styles.retryButton}
453
+ onPress={() => setPermissionDenied(false)}
454
+ >
455
+ <Text style={styles.retryButtonText}>Try Again</Text>
456
+ </TouchableOpacity>
457
+ </View>
458
+ </View>
459
+ </View>
460
+ </View>
461
+ );
462
+ }
463
+
464
+ if (isInitializingCamera || device === undefined) {
465
+ const spin = spinValue.interpolate({
466
+ inputRange: [0, 1],
467
+ outputRange: ['0deg', '360deg'],
468
+ });
469
+
470
+ return (
471
+ <View style={[styles.container]}>
472
+ <View style={styles.centeredContainer}>
473
+ <View style={styles.loadingContainer}>
474
+ <Animated.View
475
+ style={[styles.loadingSpinner, { transform: [{ rotate: spin }] }]}
476
+ />
477
+ <Text style={styles.loadingText}>
478
+ {isInitializingCamera
479
+ ? 'Initializing camera...'
480
+ : 'No camera device available'}
481
+ </Text>
482
+ </View>
483
+ </View>
484
+ </View>
485
+ );
486
+ }
487
+
488
+ const strokeDasharray = 955;
489
+
490
+ const spin = spinValue.interpolate({
491
+ inputRange: [0, 1],
492
+ outputRange: ['0deg', '360deg'],
493
+ });
494
+
495
+ return (
496
+ <View style={[styles.container]}>
497
+ <View style={styles.centeredContainer}>
498
+ {isLoading ? (
499
+ <View style={styles.loadingContainer}>
500
+ <Animated.View
501
+ style={[styles.loadingSpinner, { transform: [{ rotate: spin }] }]}
502
+ />
503
+ </View>
504
+ ) : (
505
+ <View style={styles.contentContainer}>
506
+ {capturedImage ? (
507
+ <View style={styles.capturedImageContainer}>
508
+ <View style={styles.capturedImageWrapper}>
509
+ <Image
510
+ source={{ uri: capturedImage }}
511
+ style={styles.capturedImage}
512
+ resizeMode="cover"
513
+ />
514
+ </View>
515
+
516
+ <View style={styles.capturedImageActions}>
517
+ <TouchableOpacity
518
+ style={styles.continueButton}
519
+ onPress={handleContinue}
520
+ >
521
+ <Text style={styles.continueButtonText}>Continue</Text>
522
+ </TouchableOpacity>
523
+ <TouchableOpacity
524
+ style={styles.retakeButton}
525
+ onPress={handleRetake}
526
+ >
527
+ <Text style={styles.retakeButtonText}>Retake</Text>
528
+ </TouchableOpacity>
529
+ </View>
530
+ </View>
531
+ ) : (
532
+ <>
533
+ <View style={styles.captureSection}>
534
+ <View style={styles.captureBackground}>
535
+ {isCameraActive ? (
536
+ <Camera
537
+ ref={cameraRef}
538
+ device={device}
539
+ isActive={true}
540
+ style={styles.camera}
541
+ faceDetectionCallback={handleFacesDetection}
542
+ faceDetectionOptions={faceDetectionOptions}
543
+ photo={true}
544
+ format={photoFormat}
545
+ />
546
+ ) : (
547
+ <View style={styles.avatarPlaceholder}>
548
+ <Text style={styles.avatarText}>👤</Text>
549
+ </View>
550
+ )}
551
+ </View>
552
+
553
+ {isCameraActive && (
554
+ <View style={styles.progressRingContainer}>
555
+ <Svg
556
+ width={320}
557
+ height={320}
558
+ viewBox="0 0 320 320"
559
+ style={styles.progressRing}
560
+ >
561
+ <Circle
562
+ cx={160}
563
+ cy={160}
564
+ r={150}
565
+ stroke="#E5E7EB"
566
+ strokeWidth="0"
567
+ fill="none"
568
+ />
569
+ <AnimatedCircle
570
+ cx={160}
571
+ cy={160}
572
+ r={152}
573
+ stroke="#214287"
574
+ strokeWidth={6}
575
+ fill="none"
576
+ strokeDasharray={strokeDasharray}
577
+ strokeDashoffset={progressValue}
578
+ strokeLinecap="round"
579
+ />
580
+ </Svg>
581
+ </View>
582
+ )}
583
+
584
+ {!isCameraActive && (
585
+ <View style={styles.cameraIconSection}>
586
+ <View style={styles.cameraIconContainer}>
587
+ <Text style={styles.cameraIcon}>📷</Text>
588
+ </View>
589
+ </View>
590
+ )}
591
+ </View>
592
+
593
+ {isCameraActive && (
594
+ <View style={styles.instructionContainer}>
595
+ <Text style={styles.instructionText}>
596
+ {getInstructionText()}
597
+ </Text>
598
+ </View>
599
+ )}
600
+
601
+ {!isCameraActive && !hasPermission && (
602
+ <View style={styles.startContainer}>
603
+ <TouchableOpacity
604
+ style={styles.startButton}
605
+ onPress={startCamera}
606
+ >
607
+ <Text style={styles.startButtonText}>Proceed</Text>
608
+ </TouchableOpacity>
609
+ </View>
610
+ )}
611
+ </>
612
+ )}
613
+ </View>
614
+ )}
615
+ </View>
616
+
617
+ {/* Close button */}
618
+ <TouchableOpacity style={styles.closeButton} onPress={onCancel}>
619
+ <Text style={styles.closeButtonText}>✕</Text>
620
+ </TouchableOpacity>
621
+ </View>
622
+ );
623
+ };
624
+
625
+ const styles = StyleSheet.create({
626
+ container: {
627
+ flex: 1,
628
+ marginHorizontal: 'auto',
629
+ width: '100%',
630
+ },
631
+ centeredContainer: {
632
+ flex: 1,
633
+ alignSelf: 'center',
634
+ justifyContent: 'center',
635
+ width: '100%',
636
+ paddingHorizontal: 0,
637
+ paddingBottom: 40,
638
+ paddingTop: 20,
639
+ },
640
+ contentContainer: {
641
+ flex: 1,
642
+ alignItems: 'center',
643
+ justifyContent: 'flex-start',
644
+ gap: 16,
645
+ },
646
+ captureSection: {
647
+ justifyContent: 'flex-start',
648
+ alignItems: 'center',
649
+ position: 'relative',
650
+ width: 320,
651
+ },
652
+ captureBackground: {
653
+ width: 300,
654
+ height: 300,
655
+ backgroundColor: '#F1F5F9',
656
+ overflow: 'hidden',
657
+ position: 'relative',
658
+ justifyContent: 'center',
659
+ alignItems: 'center',
660
+ borderRadius: 150,
661
+ },
662
+ camera: {
663
+ width: '100%',
664
+ height: '100%',
665
+ transform: [{ scaleX: -1 }],
666
+ },
667
+ avatarFrame: {
668
+ width: 140,
669
+ height: 140,
670
+ },
671
+ avatarPlaceholder: {
672
+ width: 140,
673
+ height: 140,
674
+ justifyContent: 'center',
675
+ alignItems: 'center',
676
+ },
677
+ avatarText: {
678
+ fontSize: 80,
679
+ },
680
+ progressRingContainer: {
681
+ position: 'absolute',
682
+ top: '50%',
683
+ left: '50%',
684
+ transform: [{ translateX: -160 }, { translateY: -160 }],
685
+ width: 320,
686
+ height: 320,
687
+ },
688
+ progressRing: {
689
+ width: '100%',
690
+ height: '100%',
691
+ },
692
+ cameraIconSection: {
693
+ position: 'absolute',
694
+ top: 240,
695
+ right: 40,
696
+ width: 42,
697
+ height: 42,
698
+ borderRadius: 21,
699
+ backgroundColor: '#ffffff',
700
+ justifyContent: 'center',
701
+ alignItems: 'center',
702
+ },
703
+ cameraIconContainer: {
704
+ width: 38,
705
+ height: 38,
706
+ borderRadius: 19,
707
+ backgroundColor: '#214287',
708
+ justifyContent: 'center',
709
+ alignItems: 'center',
710
+ },
711
+ cameraIcon: {
712
+ fontSize: 16,
713
+ color: '#ffffff',
714
+ },
715
+ instructionContainer: {
716
+ marginBottom: 16,
717
+ paddingHorizontal: 20,
718
+ alignItems: 'center',
719
+ },
720
+ instructionText: {
721
+ fontSize: 16,
722
+ color: '#667085',
723
+ textAlign: 'center',
724
+ lineHeight: 24,
725
+ },
726
+ startContainer: {
727
+ alignItems: 'center',
728
+ marginTop: 24,
729
+ },
730
+ startButton: {
731
+ backgroundColor: '#214287',
732
+ borderRadius: 10,
733
+ padding: 15,
734
+ alignItems: 'center',
735
+ minWidth: 250,
736
+ },
737
+ startButtonText: {
738
+ color: '#ffffff',
739
+ fontSize: 16,
740
+ fontWeight: '600',
741
+ textAlign: 'center',
742
+ },
743
+ loadingContainer: {
744
+ flex: 1,
745
+ justifyContent: 'center',
746
+ alignItems: 'center',
747
+ paddingVertical: 50,
748
+ },
749
+ loadingSpinner: {
750
+ width: 48,
751
+ height: 48,
752
+ borderRadius: 24,
753
+ borderWidth: 4,
754
+ borderColor: '#e5e7eb',
755
+ borderTopColor: '#214287',
756
+ marginBottom: 16,
757
+ },
758
+ loadingText: {
759
+ fontSize: 16,
760
+ color: '#667085',
761
+ textAlign: 'center',
762
+ },
763
+ capturedImageContainer: {
764
+ alignItems: 'center',
765
+ justifyContent: 'center',
766
+ gap: 24,
767
+ width: '100%',
768
+ },
769
+ capturedImageWrapper: {
770
+ width: '100%',
771
+ height: 300,
772
+ borderRadius: 12,
773
+ overflow: 'hidden',
774
+ backgroundColor: '#F1F5F9',
775
+ shadowColor: '#000',
776
+ shadowOffset: { width: 0, height: 4 },
777
+ shadowOpacity: 0.1,
778
+ shadowRadius: 8,
779
+ elevation: 5,
780
+ },
781
+ capturedImage: {
782
+ width: '100%',
783
+ height: '100%',
784
+ },
785
+ capturedImageActions: {
786
+ width: '100%',
787
+ gap: 12,
788
+ },
789
+ continueButton: {
790
+ backgroundColor: '#214287',
791
+ borderRadius: 10,
792
+ padding: 15,
793
+ alignItems: 'center',
794
+ width: '100%',
795
+ },
796
+ continueButtonText: {
797
+ color: '#ffffff',
798
+ fontSize: 16,
799
+ fontWeight: '600',
800
+ textAlign: 'center',
801
+ },
802
+ retakeButton: {
803
+ borderWidth: 1,
804
+ borderColor: '#214287',
805
+ borderRadius: 10,
806
+ padding: 15,
807
+ alignItems: 'center',
808
+ backgroundColor: 'transparent',
809
+ width: '100%',
810
+ },
811
+ retakeButtonText: {
812
+ color: '#214287',
813
+ fontSize: 16,
814
+ fontWeight: '600',
815
+ textAlign: 'center',
816
+ },
817
+ permissionDeniedContainer: {
818
+ alignItems: 'center',
819
+ },
820
+ permissionDeniedTitle: {
821
+ fontSize: 20,
822
+ fontWeight: '600',
823
+ color: '#333333',
824
+ textAlign: 'center',
825
+ marginBottom: 15,
826
+ },
827
+ permissionDeniedText: {
828
+ fontSize: 16,
829
+ color: '#666666',
830
+ textAlign: 'center',
831
+ lineHeight: 24,
832
+ marginBottom: 30,
833
+ },
834
+ permissionActions: {
835
+ width: '100%',
836
+ gap: 12,
837
+ },
838
+ settingsButton: {
839
+ backgroundColor: '#214287',
840
+ borderRadius: 10,
841
+ padding: 15,
842
+ alignItems: 'center',
843
+ width: '100%',
844
+ },
845
+ settingsButtonText: {
846
+ color: '#ffffff',
847
+ fontSize: 16,
848
+ fontWeight: '600',
849
+ textAlign: 'center',
850
+ },
851
+ retryButton: {
852
+ borderWidth: 1,
853
+ borderColor: '#214287',
854
+ borderRadius: 10,
855
+ padding: 15,
856
+ alignItems: 'center',
857
+ backgroundColor: 'transparent',
858
+ width: '100%',
859
+ },
860
+ retryButtonText: {
861
+ color: '#214287',
862
+ fontSize: 16,
863
+ fontWeight: '600',
864
+ textAlign: 'center',
865
+ },
866
+ closeButton: {
867
+ position: 'absolute',
868
+ top: 50,
869
+ right: 20,
870
+ width: 44,
871
+ height: 44,
872
+ borderRadius: 22,
873
+ backgroundColor: 'rgba(0,0,0,0.5)',
874
+ justifyContent: 'center',
875
+ alignItems: 'center',
876
+ },
877
+ closeButtonText: {
878
+ color: 'white',
879
+ fontSize: 18,
880
+ fontWeight: 'bold',
881
+ },
882
+ });
883
+
884
+ export default FaceVerification;