omnipay-reactnative-sdk 1.2.1 → 1.2.2-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.
- package/README.md +136 -45
- package/lib/commonjs/components/FaceVerification.js +755 -0
- package/lib/commonjs/components/FaceVerification.js.map +1 -0
- package/lib/commonjs/components/OmnipayProvider.js +60 -1
- package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
- package/lib/commonjs/types/faceVerification.js +2 -0
- package/lib/commonjs/types/faceVerification.js.map +1 -0
- package/lib/commonjs/types/index.js +17 -0
- package/lib/commonjs/types/index.js.map +1 -0
- package/lib/module/components/FaceVerification.js +746 -0
- package/lib/module/components/FaceVerification.js.map +1 -0
- package/lib/module/components/OmnipayProvider.js +60 -1
- package/lib/module/components/OmnipayProvider.js.map +1 -1
- package/lib/module/types/faceVerification.js +2 -0
- package/lib/module/types/faceVerification.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/typescript/components/FaceVerification.d.ts +10 -0
- package/lib/typescript/components/FaceVerification.d.ts.map +1 -0
- package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
- package/lib/typescript/types/faceVerification.d.ts +18 -0
- package/lib/typescript/types/faceVerification.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +2 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/package.json +10 -4
- package/src/components/FaceVerification.tsx +884 -0
- package/src/components/OmnipayProvider.tsx +69 -0
- package/src/types/faceVerification.ts +27 -0
- 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;
|