omnipay-reactnative-sdk 1.2.3-beta.0 → 1.2.3-beta.11
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 +102 -42
- package/android/build.gradle +6 -0
- package/android/src/main/java/com/omniretail/omnipay/FaceVerificationFrameProcessor.kt +111 -0
- package/android/src/main/java/com/omniretail/omnipay/OmnipayActivityPackage.java +5 -0
- package/ios/FaceVerificationFrameProcessor.swift +138 -0
- package/ios/FaceVerificationFrameProcessorPlugin.m +5 -0
- package/ios/OmnipayReactnativeSdk.m +5 -0
- package/ios/OmnipayReactnativeSdk.swift +10 -0
- package/ios/omnipay_reactnative_sdk.h +6 -0
- package/lib/commonjs/components/Button.js +68 -0
- package/lib/commonjs/components/Button.js.map +1 -0
- package/lib/commonjs/components/OmnipayProvider.js +6 -22
- package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
- package/lib/commonjs/components/biometrics/FaceVerification.js +111 -47
- package/lib/commonjs/components/biometrics/FaceVerification.js.map +1 -1
- package/lib/commonjs/components/biometrics/useFaceVerification.js +85 -0
- package/lib/commonjs/components/biometrics/useFaceVerification.js.map +1 -0
- package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js +157 -0
- package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js.map +1 -0
- package/lib/module/components/Button.js +61 -0
- package/lib/module/components/Button.js.map +1 -0
- package/lib/module/components/OmnipayProvider.js +6 -22
- package/lib/module/components/OmnipayProvider.js.map +1 -1
- package/lib/module/components/biometrics/FaceVerification.js +112 -49
- package/lib/module/components/biometrics/FaceVerification.js.map +1 -1
- package/lib/module/components/biometrics/useFaceVerification.js +78 -0
- package/lib/module/components/biometrics/useFaceVerification.js.map +1 -0
- package/lib/module/components/biometrics/useFaceVerificationFlow.js +150 -0
- package/lib/module/components/biometrics/useFaceVerificationFlow.js.map +1 -0
- package/lib/typescript/components/Button.d.ts +17 -0
- package/lib/typescript/components/Button.d.ts.map +1 -0
- package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
- package/lib/typescript/components/biometrics/FaceVerification.d.ts.map +1 -1
- package/lib/typescript/components/biometrics/useFaceVerification.d.ts +38 -0
- package/lib/typescript/components/biometrics/useFaceVerification.d.ts.map +1 -0
- package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts +29 -0
- package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts.map +1 -0
- package/omnipay_reactnative_sdk.podspec +46 -0
- package/package.json +14 -5
- package/src/components/Button.tsx +86 -0
- package/src/components/OmnipayProvider.tsx +6 -23
- package/src/components/biometrics/FaceVerification.tsx +134 -43
- package/src/components/biometrics/useFaceVerification.ts +120 -0
- package/src/components/biometrics/useFaceVerificationFlow.ts +224 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Modal,
|
|
@@ -8,13 +8,15 @@ import {
|
|
|
8
8
|
TouchableOpacity,
|
|
9
9
|
Image,
|
|
10
10
|
Text,
|
|
11
|
-
ActivityIndicator,
|
|
12
11
|
} from 'react-native';
|
|
13
12
|
import {
|
|
14
13
|
Camera,
|
|
15
14
|
useCameraDevice,
|
|
16
15
|
useCameraPermission,
|
|
17
16
|
} from 'react-native-vision-camera';
|
|
17
|
+
import Button from '../Button';
|
|
18
|
+
import { useFaceVerification } from './useFaceVerification';
|
|
19
|
+
import { useFaceVerificationFlow } from './useFaceVerificationFlow';
|
|
18
20
|
|
|
19
21
|
type FaceVerificationProps = {
|
|
20
22
|
onClose: () => void;
|
|
@@ -31,6 +33,26 @@ const FaceVerification: React.FC<FaceVerificationProps> = ({
|
|
|
31
33
|
const { hasPermission, requestPermission } = useCameraPermission();
|
|
32
34
|
const [isRequestingPermission, setIsRequestingPermission] = useState(false);
|
|
33
35
|
|
|
36
|
+
// Face verification flow
|
|
37
|
+
const {
|
|
38
|
+
state: flowState,
|
|
39
|
+
resetFlow,
|
|
40
|
+
isCompleted,
|
|
41
|
+
isFailed,
|
|
42
|
+
} = useFaceVerificationFlow();
|
|
43
|
+
|
|
44
|
+
// Frame processor
|
|
45
|
+
const frameProcessor = useFaceVerification();
|
|
46
|
+
|
|
47
|
+
// Handle completion
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (isCompleted) {
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
onSuccess();
|
|
52
|
+
}, 1000); // Show success message briefly
|
|
53
|
+
}
|
|
54
|
+
}, [isCompleted, onSuccess]);
|
|
55
|
+
|
|
34
56
|
const handleRequestPermission = async () => {
|
|
35
57
|
setIsRequestingPermission(true);
|
|
36
58
|
try {
|
|
@@ -52,17 +74,12 @@ const FaceVerification: React.FC<FaceVerificationProps> = ({
|
|
|
52
74
|
To verify your identity, we need access to your camera. This allows us
|
|
53
75
|
to capture your face for secure verification.
|
|
54
76
|
</Text>
|
|
55
|
-
<
|
|
56
|
-
|
|
77
|
+
<Button
|
|
78
|
+
title="Allow Camera Access"
|
|
57
79
|
onPress={handleRequestPermission}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<ActivityIndicator color="white" size="small" />
|
|
62
|
-
) : (
|
|
63
|
-
<Text style={styles.permissionButtonText}>Allow Camera Access</Text>
|
|
64
|
-
)}
|
|
65
|
-
</TouchableOpacity>
|
|
80
|
+
backgroundColor={primaryColor}
|
|
81
|
+
loading={isRequestingPermission}
|
|
82
|
+
/>
|
|
66
83
|
</View>
|
|
67
84
|
);
|
|
68
85
|
|
|
@@ -81,12 +98,60 @@ const FaceVerification: React.FC<FaceVerificationProps> = ({
|
|
|
81
98
|
|
|
82
99
|
return (
|
|
83
100
|
<View style={styles.cameraContainer}>
|
|
84
|
-
<Camera
|
|
101
|
+
<Camera
|
|
102
|
+
device={device}
|
|
103
|
+
style={styles.camera}
|
|
104
|
+
isActive={true}
|
|
105
|
+
frameProcessor={frameProcessor}
|
|
106
|
+
/>
|
|
85
107
|
<View style={styles.cameraOverlay}>
|
|
86
108
|
<View style={styles.faceFrame} />
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
|
|
110
|
+
{/* Progress indicator */}
|
|
111
|
+
<View style={styles.progressContainer}>
|
|
112
|
+
<View
|
|
113
|
+
style={[
|
|
114
|
+
styles.progressBar,
|
|
115
|
+
{
|
|
116
|
+
width: `${flowState.progress}%`,
|
|
117
|
+
backgroundColor: primaryColor,
|
|
118
|
+
},
|
|
119
|
+
]}
|
|
120
|
+
/>
|
|
121
|
+
</View>
|
|
122
|
+
|
|
123
|
+
{/* Current instruction */}
|
|
124
|
+
<Text style={styles.instructionText}>{flowState.instruction}</Text>
|
|
125
|
+
|
|
126
|
+
{/* Step indicator */}
|
|
127
|
+
<Text style={styles.stepIndicator}>
|
|
128
|
+
Step {flowState.completedSteps.length + 1} of 5
|
|
89
129
|
</Text>
|
|
130
|
+
|
|
131
|
+
{/* Success/Failure messages */}
|
|
132
|
+
{isCompleted && (
|
|
133
|
+
<View
|
|
134
|
+
style={[
|
|
135
|
+
styles.statusContainer,
|
|
136
|
+
{ backgroundColor: primaryColor },
|
|
137
|
+
]}
|
|
138
|
+
>
|
|
139
|
+
<Text style={styles.statusText}>✓ Verification Complete!</Text>
|
|
140
|
+
</View>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{isFailed && (
|
|
144
|
+
<View style={[styles.statusContainer, styles.statusContainerError]}>
|
|
145
|
+
<Text style={styles.statusText}>✗ Verification Failed</Text>
|
|
146
|
+
<Button
|
|
147
|
+
title="Try Again"
|
|
148
|
+
onPress={resetFlow}
|
|
149
|
+
backgroundColor="white"
|
|
150
|
+
textColor="#ff4444"
|
|
151
|
+
style={styles.retryButton}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
)}
|
|
90
155
|
</View>
|
|
91
156
|
</View>
|
|
92
157
|
);
|
|
@@ -228,21 +293,8 @@ const styles = StyleSheet.create({
|
|
|
228
293
|
zIndex: 2,
|
|
229
294
|
backgroundColor: 'white',
|
|
230
295
|
},
|
|
231
|
-
|
|
232
|
-
minWidth: 160,
|
|
233
|
-
marginHorizontal: 'auto',
|
|
234
|
-
},
|
|
235
|
-
button: {
|
|
236
|
-
borderRadius: 6,
|
|
237
|
-
paddingHorizontal: 12,
|
|
238
|
-
paddingVertical: 14,
|
|
239
|
-
borderWidth: 1,
|
|
240
|
-
alignItems: 'center',
|
|
241
|
-
justifyContent: 'center',
|
|
242
|
-
},
|
|
243
|
-
buttonText: { color: 'white', fontSize: 16, paddingHorizontal: 30 },
|
|
296
|
+
|
|
244
297
|
camera: {
|
|
245
|
-
flex: 1,
|
|
246
298
|
width: '100%',
|
|
247
299
|
height: 400,
|
|
248
300
|
},
|
|
@@ -280,19 +332,7 @@ const styles = StyleSheet.create({
|
|
|
280
332
|
lineHeight: 22,
|
|
281
333
|
marginBottom: 32,
|
|
282
334
|
},
|
|
283
|
-
|
|
284
|
-
paddingHorizontal: 32,
|
|
285
|
-
paddingVertical: 16,
|
|
286
|
-
borderRadius: 8,
|
|
287
|
-
minWidth: 200,
|
|
288
|
-
alignItems: 'center',
|
|
289
|
-
justifyContent: 'center',
|
|
290
|
-
},
|
|
291
|
-
permissionButtonText: {
|
|
292
|
-
color: 'white',
|
|
293
|
-
fontSize: 16,
|
|
294
|
-
fontWeight: '600',
|
|
295
|
-
},
|
|
335
|
+
|
|
296
336
|
// Error styles
|
|
297
337
|
errorTitle: {
|
|
298
338
|
fontSize: 18,
|
|
@@ -305,6 +345,7 @@ const styles = StyleSheet.create({
|
|
|
305
345
|
cameraContainer: {
|
|
306
346
|
flex: 1,
|
|
307
347
|
position: 'relative',
|
|
348
|
+
marginTop: 60,
|
|
308
349
|
},
|
|
309
350
|
cameraOverlay: {
|
|
310
351
|
position: 'absolute',
|
|
@@ -330,9 +371,59 @@ const styles = StyleSheet.create({
|
|
|
330
371
|
fontWeight: '500',
|
|
331
372
|
textAlign: 'center',
|
|
332
373
|
marginTop: 20,
|
|
333
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
374
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
334
375
|
paddingHorizontal: 16,
|
|
335
376
|
paddingVertical: 8,
|
|
336
377
|
borderRadius: 20,
|
|
337
378
|
},
|
|
379
|
+
// Verification flow styles
|
|
380
|
+
progressContainer: {
|
|
381
|
+
position: 'absolute',
|
|
382
|
+
top: 20,
|
|
383
|
+
left: 20,
|
|
384
|
+
right: 20,
|
|
385
|
+
height: 4,
|
|
386
|
+
backgroundColor: 'rgba(255,255,255,0.3)',
|
|
387
|
+
borderRadius: 2,
|
|
388
|
+
overflow: 'hidden',
|
|
389
|
+
},
|
|
390
|
+
progressBar: {
|
|
391
|
+
height: '100%',
|
|
392
|
+
borderRadius: 2,
|
|
393
|
+
},
|
|
394
|
+
stepIndicator: {
|
|
395
|
+
color: 'white',
|
|
396
|
+
fontSize: 14,
|
|
397
|
+
fontWeight: '400',
|
|
398
|
+
textAlign: 'center',
|
|
399
|
+
marginTop: 8,
|
|
400
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
401
|
+
paddingHorizontal: 12,
|
|
402
|
+
paddingVertical: 4,
|
|
403
|
+
borderRadius: 12,
|
|
404
|
+
alignSelf: 'center',
|
|
405
|
+
},
|
|
406
|
+
statusContainer: {
|
|
407
|
+
position: 'absolute',
|
|
408
|
+
bottom: 40,
|
|
409
|
+
left: 20,
|
|
410
|
+
right: 20,
|
|
411
|
+
padding: 20,
|
|
412
|
+
borderRadius: 12,
|
|
413
|
+
alignItems: 'center',
|
|
414
|
+
},
|
|
415
|
+
statusContainerError: {
|
|
416
|
+
backgroundColor: '#ff4444',
|
|
417
|
+
},
|
|
418
|
+
statusText: {
|
|
419
|
+
color: 'white',
|
|
420
|
+
fontSize: 18,
|
|
421
|
+
fontWeight: '600',
|
|
422
|
+
textAlign: 'center',
|
|
423
|
+
marginBottom: 8,
|
|
424
|
+
},
|
|
425
|
+
retryButton: {
|
|
426
|
+
marginTop: 12,
|
|
427
|
+
minWidth: 120,
|
|
428
|
+
},
|
|
338
429
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useFrameProcessor,
|
|
3
|
+
VisionCameraProxy,
|
|
4
|
+
} from 'react-native-vision-camera';
|
|
5
|
+
|
|
6
|
+
export interface FaceVerificationResult {
|
|
7
|
+
faceDetected: boolean;
|
|
8
|
+
isSmiling?: boolean;
|
|
9
|
+
isBlinking?: boolean;
|
|
10
|
+
leftEyeClosed?: boolean;
|
|
11
|
+
rightEyeClosed?: boolean;
|
|
12
|
+
headPose?: {
|
|
13
|
+
yaw: number; // Left-right movement (-90 to 90)
|
|
14
|
+
pitch: number; // Up-down movement (-90 to 90)
|
|
15
|
+
roll: number; // Tilt movement (-180 to 180)
|
|
16
|
+
};
|
|
17
|
+
boundingBox?: {
|
|
18
|
+
x?: number;
|
|
19
|
+
y?: number;
|
|
20
|
+
width?: number;
|
|
21
|
+
height?: number;
|
|
22
|
+
left?: number;
|
|
23
|
+
top?: number;
|
|
24
|
+
right?: number;
|
|
25
|
+
bottom?: number;
|
|
26
|
+
};
|
|
27
|
+
smileProbability?: number;
|
|
28
|
+
leftEyeOpenProbability?: number;
|
|
29
|
+
rightEyeOpenProbability?: number;
|
|
30
|
+
trackingId?: number;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Note: For production use, you would typically use shared values or state management
|
|
35
|
+
// to communicate results from the worklet back to the React component.
|
|
36
|
+
// This simplified version just logs results for debugging.
|
|
37
|
+
|
|
38
|
+
// Initialize the plugin
|
|
39
|
+
const plugin = VisionCameraProxy.initFrameProcessorPlugin('detectFaces', {});
|
|
40
|
+
|
|
41
|
+
export const useFaceVerification = () => {
|
|
42
|
+
const frameProcessor = useFrameProcessor((frame) => {
|
|
43
|
+
'worklet';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (plugin == null) {
|
|
47
|
+
console.error('Failed to load Frame Processor Plugin "detectFaces"!');
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = plugin.call(frame, {}) as any;
|
|
52
|
+
|
|
53
|
+
// For debugging - this works in worklets
|
|
54
|
+
if (result && result.faceDetected) {
|
|
55
|
+
console.log('Face detected:', result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Face verification error:', error);
|
|
61
|
+
return {
|
|
62
|
+
faceDetected: false,
|
|
63
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return frameProcessor;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Utility functions for interpreting results
|
|
72
|
+
export const FaceVerificationUtils = {
|
|
73
|
+
// Check if head is turned left (yaw > 20 degrees)
|
|
74
|
+
isHeadTurnedLeft: (result: FaceVerificationResult): boolean => {
|
|
75
|
+
return result.headPose?.yaw ? result.headPose.yaw > 20 : false;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Check if head is turned right (yaw < -20 degrees)
|
|
79
|
+
isHeadTurnedRight: (result: FaceVerificationResult): boolean => {
|
|
80
|
+
return result.headPose?.yaw ? result.headPose.yaw < -20 : false;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Check if head is tilted up (pitch > 10 degrees)
|
|
84
|
+
isHeadTiltedUp: (result: FaceVerificationResult): boolean => {
|
|
85
|
+
return result.headPose?.pitch ? result.headPose.pitch > 10 : false;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Check if head is tilted down (pitch < -10 degrees)
|
|
89
|
+
isHeadTiltedDown: (result: FaceVerificationResult): boolean => {
|
|
90
|
+
return result.headPose?.pitch ? result.headPose.pitch < -10 : false;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Check if face is centered (within ±10 degrees)
|
|
94
|
+
isFaceCentered: (result: FaceVerificationResult): boolean => {
|
|
95
|
+
if (!result.headPose) return false;
|
|
96
|
+
const { yaw, pitch } = result.headPose;
|
|
97
|
+
return Math.abs(yaw) <= 10 && Math.abs(pitch) <= 10;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Check if smile is confident (high probability)
|
|
101
|
+
isConfidentSmile: (result: FaceVerificationResult): boolean => {
|
|
102
|
+
return result.smileProbability
|
|
103
|
+
? result.smileProbability > 0.8
|
|
104
|
+
: result.isSmiling || false;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Check if both eyes are confidently closed
|
|
108
|
+
isConfidentBlink: (result: FaceVerificationResult): boolean => {
|
|
109
|
+
if (
|
|
110
|
+
result.leftEyeOpenProbability !== undefined &&
|
|
111
|
+
result.rightEyeOpenProbability !== undefined
|
|
112
|
+
) {
|
|
113
|
+
return (
|
|
114
|
+
result.leftEyeOpenProbability < 0.2 &&
|
|
115
|
+
result.rightEyeOpenProbability < 0.2
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return result.isBlinking || false;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FaceVerificationResult,
|
|
4
|
+
FaceVerificationUtils,
|
|
5
|
+
} from './useFaceVerification';
|
|
6
|
+
|
|
7
|
+
export type VerificationStep =
|
|
8
|
+
| 'position_face'
|
|
9
|
+
| 'smile'
|
|
10
|
+
| 'blink'
|
|
11
|
+
| 'turn_left'
|
|
12
|
+
| 'turn_right'
|
|
13
|
+
| 'completed'
|
|
14
|
+
| 'failed';
|
|
15
|
+
|
|
16
|
+
export interface VerificationFlowState {
|
|
17
|
+
currentStep: VerificationStep;
|
|
18
|
+
completedSteps: VerificationStep[];
|
|
19
|
+
stepStartTime: number;
|
|
20
|
+
totalStartTime: number;
|
|
21
|
+
isProcessing: boolean;
|
|
22
|
+
progress: number;
|
|
23
|
+
instruction: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface VerificationFlowConfig {
|
|
28
|
+
stepTimeout: number; // Maximum time per step (ms)
|
|
29
|
+
totalTimeout: number; // Maximum total time (ms)
|
|
30
|
+
confirmationFrames: number; // Frames needed to confirm action
|
|
31
|
+
requiredSteps: VerificationStep[]; // Steps to complete
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONFIG: VerificationFlowConfig = {
|
|
35
|
+
stepTimeout: 10000, // 10 seconds per step
|
|
36
|
+
totalTimeout: 60000, // 1 minute total
|
|
37
|
+
confirmationFrames: 5, // 5 consecutive frames
|
|
38
|
+
requiredSteps: ['position_face', 'smile', 'blink', 'turn_left', 'turn_right'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const STEP_INSTRUCTIONS: Record<VerificationStep, string> = {
|
|
42
|
+
position_face: 'Position your face in the center of the frame',
|
|
43
|
+
smile: 'Please smile for the camera',
|
|
44
|
+
blink: 'Please blink your eyes',
|
|
45
|
+
turn_left: 'Slowly turn your head to the left',
|
|
46
|
+
turn_right: 'Slowly turn your head to the right',
|
|
47
|
+
completed: 'Verification completed successfully!',
|
|
48
|
+
failed: 'Verification failed. Please try again.',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const useFaceVerificationFlow = (
|
|
52
|
+
config: Partial<VerificationFlowConfig> = {}
|
|
53
|
+
) => {
|
|
54
|
+
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
|
55
|
+
|
|
56
|
+
const [state, setState] = useState<VerificationFlowState>({
|
|
57
|
+
currentStep: 'position_face',
|
|
58
|
+
completedSteps: [],
|
|
59
|
+
stepStartTime: Date.now(),
|
|
60
|
+
totalStartTime: Date.now(),
|
|
61
|
+
isProcessing: false,
|
|
62
|
+
progress: 0,
|
|
63
|
+
instruction: STEP_INSTRUCTIONS.position_face,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [confirmationCount, setConfirmationCount] = useState(0);
|
|
67
|
+
|
|
68
|
+
// Reset verification flow
|
|
69
|
+
const resetFlow = useCallback(() => {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
setState({
|
|
72
|
+
currentStep: 'position_face',
|
|
73
|
+
completedSteps: [],
|
|
74
|
+
stepStartTime: now,
|
|
75
|
+
totalStartTime: now,
|
|
76
|
+
isProcessing: false,
|
|
77
|
+
progress: 0,
|
|
78
|
+
instruction: STEP_INSTRUCTIONS.position_face,
|
|
79
|
+
});
|
|
80
|
+
setConfirmationCount(0);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// Check if current step is completed based on face detection result
|
|
84
|
+
const checkStepCompletion = useCallback(
|
|
85
|
+
(result: FaceVerificationResult): boolean => {
|
|
86
|
+
if (!result.faceDetected) return false;
|
|
87
|
+
|
|
88
|
+
switch (state.currentStep) {
|
|
89
|
+
case 'position_face':
|
|
90
|
+
return FaceVerificationUtils.isFaceCentered(result);
|
|
91
|
+
|
|
92
|
+
case 'smile':
|
|
93
|
+
return FaceVerificationUtils.isConfidentSmile(result);
|
|
94
|
+
|
|
95
|
+
case 'blink':
|
|
96
|
+
return FaceVerificationUtils.isConfidentBlink(result);
|
|
97
|
+
|
|
98
|
+
case 'turn_left':
|
|
99
|
+
return FaceVerificationUtils.isHeadTurnedLeft(result);
|
|
100
|
+
|
|
101
|
+
case 'turn_right':
|
|
102
|
+
return FaceVerificationUtils.isHeadTurnedRight(result);
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[state.currentStep]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Get next step in sequence
|
|
112
|
+
const getNextStep = useCallback(
|
|
113
|
+
(currentStep: VerificationStep): VerificationStep => {
|
|
114
|
+
const currentIndex = fullConfig.requiredSteps.indexOf(currentStep);
|
|
115
|
+
if (
|
|
116
|
+
currentIndex === -1 ||
|
|
117
|
+
currentIndex === fullConfig.requiredSteps.length - 1
|
|
118
|
+
) {
|
|
119
|
+
return 'completed';
|
|
120
|
+
}
|
|
121
|
+
const nextStep = fullConfig.requiredSteps[currentIndex + 1];
|
|
122
|
+
return nextStep || 'completed';
|
|
123
|
+
},
|
|
124
|
+
[fullConfig.requiredSteps]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Process face detection result
|
|
128
|
+
const processFaceResult = useCallback(
|
|
129
|
+
(result: FaceVerificationResult) => {
|
|
130
|
+
if (state.currentStep === 'completed' || state.currentStep === 'failed') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
|
|
136
|
+
// Check for timeouts
|
|
137
|
+
if (now - state.totalStartTime > fullConfig.totalTimeout) {
|
|
138
|
+
setState((prev) => ({
|
|
139
|
+
...prev,
|
|
140
|
+
currentStep: 'failed',
|
|
141
|
+
instruction: 'Verification timed out. Please try again.',
|
|
142
|
+
error: 'Total verification timeout exceeded',
|
|
143
|
+
}));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (now - state.stepStartTime > fullConfig.stepTimeout) {
|
|
148
|
+
setState((prev) => ({
|
|
149
|
+
...prev,
|
|
150
|
+
currentStep: 'failed',
|
|
151
|
+
instruction: 'Step timed out. Please try again.',
|
|
152
|
+
error: 'Step timeout exceeded',
|
|
153
|
+
}));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if current step is completed
|
|
158
|
+
const stepCompleted = checkStepCompletion(result);
|
|
159
|
+
|
|
160
|
+
if (stepCompleted) {
|
|
161
|
+
setConfirmationCount((prev) => prev + 1);
|
|
162
|
+
|
|
163
|
+
// Require multiple consecutive confirmations
|
|
164
|
+
if (confirmationCount + 1 >= fullConfig.confirmationFrames) {
|
|
165
|
+
const nextStep = getNextStep(state.currentStep);
|
|
166
|
+
const newCompletedSteps = [
|
|
167
|
+
...state.completedSteps,
|
|
168
|
+
state.currentStep,
|
|
169
|
+
];
|
|
170
|
+
const newProgress =
|
|
171
|
+
(newCompletedSteps.length / fullConfig.requiredSteps.length) * 100;
|
|
172
|
+
|
|
173
|
+
setState((prev) => ({
|
|
174
|
+
...prev,
|
|
175
|
+
currentStep: nextStep,
|
|
176
|
+
completedSteps: newCompletedSteps,
|
|
177
|
+
stepStartTime: now,
|
|
178
|
+
progress: newProgress,
|
|
179
|
+
instruction: STEP_INSTRUCTIONS[nextStep],
|
|
180
|
+
isProcessing: nextStep === 'completed',
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
setConfirmationCount(0);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Reset confirmation count if step not completed
|
|
187
|
+
setConfirmationCount(0);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
[
|
|
191
|
+
state,
|
|
192
|
+
fullConfig.totalTimeout,
|
|
193
|
+
fullConfig.stepTimeout,
|
|
194
|
+
fullConfig.confirmationFrames,
|
|
195
|
+
fullConfig.requiredSteps.length,
|
|
196
|
+
confirmationCount,
|
|
197
|
+
checkStepCompletion,
|
|
198
|
+
getNextStep,
|
|
199
|
+
]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Auto-reset on mount
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
resetFlow();
|
|
205
|
+
}, [resetFlow]);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
state,
|
|
209
|
+
processFaceResult,
|
|
210
|
+
resetFlow,
|
|
211
|
+
isCompleted: state.currentStep === 'completed',
|
|
212
|
+
isFailed: state.currentStep === 'failed',
|
|
213
|
+
isActive:
|
|
214
|
+
state.currentStep !== 'completed' && state.currentStep !== 'failed',
|
|
215
|
+
remainingTime: Math.max(
|
|
216
|
+
0,
|
|
217
|
+
fullConfig.stepTimeout - (Date.now() - state.stepStartTime)
|
|
218
|
+
),
|
|
219
|
+
totalRemainingTime: Math.max(
|
|
220
|
+
0,
|
|
221
|
+
fullConfig.totalTimeout - (Date.now() - state.totalStartTime)
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
};
|