omnipay-reactnative-sdk 1.2.3-beta.0 → 1.2.3-beta.10

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 (44) hide show
  1. package/README.md +102 -42
  2. package/android/build.gradle +6 -0
  3. package/android/src/main/java/com/omniretail/omnipay/FaceVerificationFrameProcessor.kt +111 -0
  4. package/android/src/main/java/com/omniretail/omnipay/OmnipayActivityPackage.java +5 -0
  5. package/ios/FaceVerificationFrameProcessor.swift +138 -0
  6. package/ios/FaceVerificationFrameProcessorPlugin.m +4 -0
  7. package/ios/OmnipayReactnativeSdk.m +5 -0
  8. package/ios/OmnipayReactnativeSdk.swift +10 -0
  9. package/ios/omnipay_reactnative_sdk.h +6 -0
  10. package/lib/commonjs/components/Button.js +68 -0
  11. package/lib/commonjs/components/Button.js.map +1 -0
  12. package/lib/commonjs/components/OmnipayProvider.js +6 -22
  13. package/lib/commonjs/components/OmnipayProvider.js.map +1 -1
  14. package/lib/commonjs/components/biometrics/FaceVerification.js +111 -47
  15. package/lib/commonjs/components/biometrics/FaceVerification.js.map +1 -1
  16. package/lib/commonjs/components/biometrics/useFaceVerification.js +85 -0
  17. package/lib/commonjs/components/biometrics/useFaceVerification.js.map +1 -0
  18. package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js +157 -0
  19. package/lib/commonjs/components/biometrics/useFaceVerificationFlow.js.map +1 -0
  20. package/lib/module/components/Button.js +61 -0
  21. package/lib/module/components/Button.js.map +1 -0
  22. package/lib/module/components/OmnipayProvider.js +6 -22
  23. package/lib/module/components/OmnipayProvider.js.map +1 -1
  24. package/lib/module/components/biometrics/FaceVerification.js +112 -49
  25. package/lib/module/components/biometrics/FaceVerification.js.map +1 -1
  26. package/lib/module/components/biometrics/useFaceVerification.js +78 -0
  27. package/lib/module/components/biometrics/useFaceVerification.js.map +1 -0
  28. package/lib/module/components/biometrics/useFaceVerificationFlow.js +150 -0
  29. package/lib/module/components/biometrics/useFaceVerificationFlow.js.map +1 -0
  30. package/lib/typescript/components/Button.d.ts +17 -0
  31. package/lib/typescript/components/Button.d.ts.map +1 -0
  32. package/lib/typescript/components/OmnipayProvider.d.ts.map +1 -1
  33. package/lib/typescript/components/biometrics/FaceVerification.d.ts.map +1 -1
  34. package/lib/typescript/components/biometrics/useFaceVerification.d.ts +38 -0
  35. package/lib/typescript/components/biometrics/useFaceVerification.d.ts.map +1 -0
  36. package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts +29 -0
  37. package/lib/typescript/components/biometrics/useFaceVerificationFlow.d.ts.map +1 -0
  38. package/omnipay_reactnative_sdk.podspec +46 -0
  39. package/package.json +14 -5
  40. package/src/components/Button.tsx +86 -0
  41. package/src/components/OmnipayProvider.tsx +6 -23
  42. package/src/components/biometrics/FaceVerification.tsx +134 -43
  43. package/src/components/biometrics/useFaceVerification.ts +120 -0
  44. 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
- <TouchableOpacity
56
- style={[styles.permissionButton, { backgroundColor: primaryColor }]}
77
+ <Button
78
+ title="Allow Camera Access"
57
79
  onPress={handleRequestPermission}
58
- disabled={isRequestingPermission}
59
- >
60
- {isRequestingPermission ? (
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 device={device} style={styles.camera} isActive={true} />
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
- <Text style={styles.instructionText}>
88
- Position your face within the frame
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
- retryButton: {
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
- permissionButton: {
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.5)',
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
+ };