@transfergratis/react-native-sdk 0.1.25 → 0.1.28
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/android/src/main/AndroidManifest.xml +12 -0
- package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/components/EnhancedCameraView.web.js +76 -21
- package/build/components/EnhancedCameraView.web.js.map +1 -1
- package/build/components/KYCElements/EmailVerificationTemplate.d.ts.map +1 -1
- package/build/components/KYCElements/EmailVerificationTemplate.js +48 -29
- package/build/components/KYCElements/EmailVerificationTemplate.js.map +1 -1
- package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/components/KYCElements/IDCardCapture.js +48 -11
- package/build/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/components/KYCElements/WelcomeTemplate.js +2 -1
- package/build/components/KYCElements/WelcomeTemplate.js.map +1 -1
- package/build/components/OverLay/type.d.ts +2 -0
- package/build/components/OverLay/type.d.ts.map +1 -1
- package/build/components/OverLay/type.js.map +1 -1
- package/build/components/TemplateKYCExample.d.ts +8 -2
- package/build/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/components/TemplateKYCExample.js +2 -2
- package/build/components/TemplateKYCExample.js.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.d.ts +10 -2
- package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/components/TemplateKYCFlowRefactored.js +13 -3
- package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
- package/build/config/KYCConfig.js +1 -1
- package/build/config/KYCConfig.js.map +1 -1
- package/build/hooks/useTemplateKYCFlow.d.ts +14 -2
- package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/hooks/useTemplateKYCFlow.js +175 -84
- package/build/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/i18n/en/index.d.ts +2 -0
- package/build/i18n/en/index.d.ts.map +1 -1
- package/build/i18n/en/index.js +3 -1
- package/build/i18n/en/index.js.map +1 -1
- package/build/i18n/fr/index.d.ts +2 -0
- package/build/i18n/fr/index.d.ts.map +1 -1
- package/build/i18n/fr/index.js +3 -1
- package/build/i18n/fr/index.js.map +1 -1
- package/build/i18n/types.d.ts +2 -0
- package/build/i18n/types.d.ts.map +1 -1
- package/build/i18n/types.js.map +1 -1
- package/build/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/modules/api/CardAuthentification.js +28 -2
- package/build/modules/api/CardAuthentification.js.map +1 -1
- package/build/modules/api/KYCService.d.ts +10 -0
- package/build/modules/api/KYCService.d.ts.map +1 -1
- package/build/modules/api/KYCService.js +24 -0
- package/build/modules/api/KYCService.js.map +1 -1
- package/build/modules/camera/VisionCameraModule.web.d.ts.map +1 -1
- package/build/modules/camera/VisionCameraModule.web.js +27 -8
- package/build/modules/camera/VisionCameraModule.web.js.map +1 -1
- package/build/types/KYC.types.d.ts +6 -2
- package/build/types/KYC.types.d.ts.map +1 -1
- package/build/types/KYC.types.js.map +1 -1
- package/build/utils/cropByObb.d.ts +17 -0
- package/build/utils/cropByObb.d.ts.map +1 -1
- package/build/utils/cropByObb.js +51 -1
- package/build/utils/cropByObb.js.map +1 -1
- package/build/web/WebKYCEntry.d.ts.map +1 -1
- package/build/web/WebKYCEntry.js +11 -5
- package/build/web/WebKYCEntry.js.map +1 -1
- package/package.json +1 -1
- package/plugin/build/index.d.ts +1 -0
- package/plugin/build/index.js +3 -1
- package/plugin/build/withRemovePermissions.d.ts +3 -0
- package/plugin/build/withRemovePermissions.js +67 -0
- package/plugin/src/index.ts +2 -1
- package/plugin/src/withRemovePermissions.js +85 -0
- package/plugin/src/withRemovePermissions.ts +83 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/plugin.js +6 -1
- package/src/components/EnhancedCameraView.web.tsx +76 -21
- package/src/components/KYCElements/EmailVerificationTemplate.tsx +47 -33
- package/src/components/KYCElements/IDCardCapture.tsx +51 -10
- package/src/components/KYCElements/WelcomeTemplate.tsx +2 -1
- package/src/components/OverLay/type.ts +2 -0
- package/src/components/TemplateKYCExample.tsx +9 -5
- package/src/components/TemplateKYCFlowRefactored.tsx +24 -6
- package/src/config/KYCConfig.ts +1 -1
- package/src/hooks/useTemplateKYCFlow.tsx +189 -95
- package/src/i18n/en/index.ts +3 -1
- package/src/i18n/fr/index.ts +3 -1
- package/src/i18n/types.ts +2 -0
- package/src/modules/api/CardAuthentification.ts +30 -2
- package/src/modules/api/KYCService.ts +41 -0
- package/src/modules/camera/VisionCameraModule.web.ts +30 -12
- package/src/types/KYC.types.ts +7 -3
- package/src/utils/cropByObb.ts +57 -1
- package/src/web/WebKYCEntry.tsx +17 -6
|
@@ -27,6 +27,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
27
27
|
videoDuration = 10,
|
|
28
28
|
onSilentCapture,
|
|
29
29
|
silentCaptureResult,
|
|
30
|
+
captureStabilizationDelayMs = 2500,
|
|
30
31
|
}) => {
|
|
31
32
|
const { t } = useI18n();
|
|
32
33
|
|
|
@@ -63,6 +64,24 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
63
64
|
};
|
|
64
65
|
}, [stream]);
|
|
65
66
|
|
|
67
|
+
// Build video constraints for quality (min + ideal improve Android/Samsung web quality; higher ideal for documents)
|
|
68
|
+
const getVideoConstraints = useCallback((): MediaTrackConstraints => {
|
|
69
|
+
const isHigh = quality === 'high';
|
|
70
|
+
const isMedium = quality === 'medium';
|
|
71
|
+
return {
|
|
72
|
+
facingMode: cameraType === 'front' ? 'user' : 'environment',
|
|
73
|
+
// min forces Android Chrome to use at least this resolution (avoids 640x480 default)
|
|
74
|
+
width: {
|
|
75
|
+
min: isHigh ? 1280 : isMedium ? 960 : 640,
|
|
76
|
+
ideal: isHigh ? 2560 : isMedium ? 1280 : 640,
|
|
77
|
+
},
|
|
78
|
+
height: {
|
|
79
|
+
min: isHigh ? 720 : isMedium ? 540 : 480,
|
|
80
|
+
ideal: isHigh ? 1440 : isMedium ? 720 : 480,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}, [cameraType, quality]);
|
|
84
|
+
|
|
66
85
|
const checkPermissions = async () => {
|
|
67
86
|
try {
|
|
68
87
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
@@ -70,8 +89,11 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
70
89
|
return;
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
//
|
|
74
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
92
|
+
// Use same constraints as startCamera so Android Chrome doesn't cache low resolution (e.g. 640x480)
|
|
93
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
94
|
+
video: getVideoConstraints(),
|
|
95
|
+
audio: enableVideo,
|
|
96
|
+
});
|
|
75
97
|
stream.getTracks().forEach(track => track.stop());
|
|
76
98
|
|
|
77
99
|
setHasPermission(true);
|
|
@@ -88,16 +110,32 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
88
110
|
stream.getTracks().forEach(track => track.stop());
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
const constraints = {
|
|
92
|
-
video:
|
|
93
|
-
facingMode: cameraType === 'front' ? 'user' : 'environment',
|
|
94
|
-
width: { ideal: quality === 'high' ? 1920 : quality === 'medium' ? 1280 : 640 },
|
|
95
|
-
height: { ideal: quality === 'high' ? 1080 : quality === 'medium' ? 720 : 480 },
|
|
96
|
-
},
|
|
113
|
+
const constraints: MediaStreamConstraints = {
|
|
114
|
+
video: getVideoConstraints(),
|
|
97
115
|
audio: enableVideo,
|
|
98
116
|
};
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
let newStream: MediaStream;
|
|
119
|
+
try {
|
|
120
|
+
newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const name = err instanceof Error ? err.name : '';
|
|
123
|
+
// On some Android devices, min constraints can fail; fallback to ideal only
|
|
124
|
+
if (name === 'OverconstrainedError') {
|
|
125
|
+
const fallbackConstraints: MediaStreamConstraints = {
|
|
126
|
+
video: {
|
|
127
|
+
facingMode: cameraType === 'front' ? 'user' : 'environment',
|
|
128
|
+
width: { ideal: quality === 'high' ? 2560 : quality === 'medium' ? 1280 : 640 },
|
|
129
|
+
height: { ideal: quality === 'high' ? 1440 : quality === 'medium' ? 720 : 480 },
|
|
130
|
+
},
|
|
131
|
+
audio: enableVideo,
|
|
132
|
+
};
|
|
133
|
+
newStream = await navigator.mediaDevices.getUserMedia(fallbackConstraints);
|
|
134
|
+
} else {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
setStream(newStream);
|
|
102
140
|
|
|
103
141
|
if (videoRef.current) {
|
|
@@ -159,11 +197,19 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
159
197
|
canvas.width = video.videoWidth;
|
|
160
198
|
canvas.height = video.videoHeight;
|
|
161
199
|
|
|
162
|
-
//
|
|
163
|
-
|
|
200
|
+
// For front camera: draw flipped so captured image matches non-mirrored preview (no mirror confusion)
|
|
201
|
+
if (cameraType === 'front') {
|
|
202
|
+
context.save();
|
|
203
|
+
context.translate(canvas.width, 0);
|
|
204
|
+
context.scale(-1, 1);
|
|
205
|
+
context.drawImage(video, 0, 0);
|
|
206
|
+
context.restore();
|
|
207
|
+
} else {
|
|
208
|
+
context.drawImage(video, 0, 0);
|
|
209
|
+
}
|
|
164
210
|
|
|
165
|
-
// Convert to base64
|
|
166
|
-
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.
|
|
211
|
+
// Convert to base64 (0.95 for document/ID capture clarity; backend can exploit text better)
|
|
212
|
+
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.95);
|
|
167
213
|
|
|
168
214
|
onSilentCapture?.({
|
|
169
215
|
success: true,
|
|
@@ -176,21 +222,29 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
176
222
|
error: error instanceof Error ? error.message : 'Failed to capture photo',
|
|
177
223
|
});
|
|
178
224
|
}
|
|
179
|
-
}, [isInitialized, onError, onSilentCapture]);
|
|
225
|
+
}, [isInitialized, cameraType, onError, onSilentCapture]);
|
|
180
226
|
|
|
181
227
|
|
|
182
|
-
//
|
|
228
|
+
// Stabilization delay then auto-capture every 5s; stop as soon as capture is validated (no more new captures)
|
|
183
229
|
useEffect(() => {
|
|
184
|
-
if (!showCamera || !isInitialized) {
|
|
230
|
+
if (!showCamera || !isInitialized || silentCaptureResult?.success) {
|
|
185
231
|
return;
|
|
186
232
|
}
|
|
187
233
|
|
|
188
|
-
const
|
|
234
|
+
const delayMs = Math.max(0, captureStabilizationDelayMs);
|
|
235
|
+
const intervalMs = 5000;
|
|
236
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
237
|
+
|
|
238
|
+
const timeoutId = setTimeout(() => {
|
|
189
239
|
captureSilentPhoto();
|
|
190
|
-
|
|
240
|
+
intervalId = setInterval(captureSilentPhoto, intervalMs);
|
|
241
|
+
}, delayMs);
|
|
191
242
|
|
|
192
|
-
return () =>
|
|
193
|
-
|
|
243
|
+
return () => {
|
|
244
|
+
clearTimeout(timeoutId);
|
|
245
|
+
if (intervalId) clearInterval(intervalId);
|
|
246
|
+
};
|
|
247
|
+
}, [showCamera, isInitialized, captureStabilizationDelayMs, captureSilentPhoto, silentCaptureResult?.success]);
|
|
194
248
|
|
|
195
249
|
const startVideoRecording = useCallback(async () => {
|
|
196
250
|
try {
|
|
@@ -299,7 +353,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
299
353
|
|
|
300
354
|
return (
|
|
301
355
|
<View style={[styles.container, style]}>
|
|
302
|
-
{/* Video element */}
|
|
356
|
+
{/* Video element; no mirror for front camera so preview matches final photo and doesn't confuse users */}
|
|
303
357
|
<video
|
|
304
358
|
ref={videoRef}
|
|
305
359
|
style={{
|
|
@@ -307,6 +361,7 @@ export const EnhancedCameraView: React.FC<EnhancedCameraViewProps> = ({
|
|
|
307
361
|
width: '100%',
|
|
308
362
|
height: '100%',
|
|
309
363
|
objectFit: 'cover',
|
|
364
|
+
transform: cameraType === 'front' ? 'scaleX(-1)' : undefined,
|
|
310
365
|
}}
|
|
311
366
|
autoPlay
|
|
312
367
|
playsInline
|
|
@@ -4,6 +4,7 @@ import { TemplateComponent, LocalizedText } from '../../types/KYC.types';
|
|
|
4
4
|
import { useTemplateKYCFlowContext } from '../../hooks/useTemplateKYCFlow';
|
|
5
5
|
import { useI18n } from '../../hooks/useI18n';
|
|
6
6
|
import { Button } from '../ui/Button';
|
|
7
|
+
import kycService, { errorMessage } from '../../modules/api/KYCService';
|
|
7
8
|
|
|
8
9
|
interface EmailVerificationTemplateProps {
|
|
9
10
|
component: TemplateComponent;
|
|
@@ -15,14 +16,21 @@ interface EmailVerificationTemplateProps {
|
|
|
15
16
|
|
|
16
17
|
type VerificationStep = 'email' | 'otp';
|
|
17
18
|
|
|
19
|
+
/** RFC-style email validation: local@domain.tld */
|
|
20
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
21
|
+
|
|
22
|
+
const isValidEmail = (value: string): boolean => EMAIL_REGEX.test((value || '').trim());
|
|
23
|
+
|
|
18
24
|
export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps> = ({
|
|
19
25
|
component,
|
|
20
26
|
value,
|
|
21
27
|
onValueChange,
|
|
22
28
|
error: propError,
|
|
23
29
|
}) => {
|
|
24
|
-
const { actions, getLocalizedText } = useTemplateKYCFlowContext();
|
|
30
|
+
const { actions, getLocalizedText, state, apiKey } = useTemplateKYCFlowContext();
|
|
25
31
|
const { t } = useI18n();
|
|
32
|
+
|
|
33
|
+
const auth = apiKey ? { apiKey } : (state.session.token ? { token: state.session.token } : undefined);
|
|
26
34
|
// const config = component.config as EmailVerificationConfig; // Keep for future use
|
|
27
35
|
|
|
28
36
|
// State
|
|
@@ -40,8 +48,9 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
40
48
|
const sendButtonText = t('common.sendCode') || 'Send Verification Code';
|
|
41
49
|
const buttonText = step === 'email' ? sendButtonText : verifyButtonText;
|
|
42
50
|
|
|
43
|
-
const handleSendCode = () => {
|
|
44
|
-
|
|
51
|
+
const handleSendCode = async () => {
|
|
52
|
+
const trimmed = email.trim();
|
|
53
|
+
if (!trimmed || !isValidEmail(trimmed)) {
|
|
45
54
|
setLocalError(t('errors.invalidEmail') || 'Please enter a valid email address');
|
|
46
55
|
return;
|
|
47
56
|
}
|
|
@@ -49,16 +58,18 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
49
58
|
setLocalError(null);
|
|
50
59
|
setIsSimulating(true);
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setIsSimulating(false);
|
|
61
|
+
try {
|
|
62
|
+
await kycService.sendEmailVerificationCode(trimmed, auth);
|
|
55
63
|
setStep('otp');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send verification code');
|
|
66
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
67
|
+
} finally {
|
|
68
|
+
setIsSimulating(false);
|
|
69
|
+
}
|
|
59
70
|
};
|
|
60
71
|
|
|
61
|
-
const handleVerifyCode = () => {
|
|
72
|
+
const handleVerifyCode = async () => {
|
|
62
73
|
if (!otp || otp.length < 4) {
|
|
63
74
|
setLocalError(t('errors.invalidCode') || 'Please enter the 6-digit code');
|
|
64
75
|
return;
|
|
@@ -67,24 +78,17 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
67
78
|
setLocalError(null);
|
|
68
79
|
setIsSimulating(true);
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
try {
|
|
82
|
+
await kycService.verifyEmailCode(otp.trim(), auth);
|
|
83
|
+
const data = { email, otp, verified: true };
|
|
84
|
+
onValueChange(data);
|
|
85
|
+
actions.nextComponent(data);
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.wrongCode') || 'Invalid verification code');
|
|
88
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
89
|
+
} finally {
|
|
72
90
|
setIsSimulating(false);
|
|
73
|
-
|
|
74
|
-
// Mock validation logic
|
|
75
|
-
// Let's accept '123456' as the correct code or any code for testing if strictly requested?
|
|
76
|
-
// User said "verify with error message" implying we should support failure.
|
|
77
|
-
// Let's say if code is "000000" it fails, otherwise success, OR hardcode a success one.
|
|
78
|
-
// User said "verify with error message and next component if is a good one"
|
|
79
|
-
// Let's make "123456" the good one for clarity.
|
|
80
|
-
|
|
81
|
-
if (otp === '123456') {
|
|
82
|
-
onValueChange({ email, otp, verified: true });
|
|
83
|
-
actions.nextComponent();
|
|
84
|
-
} else {
|
|
85
|
-
setLocalError(t('errors.wrongCode') || 'Invalid verification code. Try 123456');
|
|
86
|
-
}
|
|
87
|
-
}, 1500);
|
|
91
|
+
}
|
|
88
92
|
};
|
|
89
93
|
|
|
90
94
|
const onChangeEmail = (text: string) => {
|
|
@@ -160,12 +164,22 @@ export const EmailVerificationTemplate: React.FC<EmailVerificationTemplateProps>
|
|
|
160
164
|
|
|
161
165
|
{step === 'otp' && (
|
|
162
166
|
<TouchableOpacity
|
|
163
|
-
onPress={() => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
onPress={async () => {
|
|
168
|
+
if (isSimulating) return;
|
|
169
|
+
setLocalError(null);
|
|
170
|
+
setIsSimulating(true);
|
|
171
|
+
try {
|
|
172
|
+
await kycService.sendEmailVerificationCode(email.trim(), auth);
|
|
173
|
+
Alert.alert(
|
|
174
|
+
t('common.codeResent') || 'Code Resent',
|
|
175
|
+
t('common.codeResentMessage', { email }) || 'Code resent to ' + email
|
|
176
|
+
);
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
const msg = errorMessage(err) ?? err?.message ?? (t('errors.sendCodeFailed') || 'Failed to send code');
|
|
179
|
+
setLocalError(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
180
|
+
} finally {
|
|
181
|
+
setIsSimulating(false);
|
|
182
|
+
}
|
|
169
183
|
}}
|
|
170
184
|
style={styles.resendButton}
|
|
171
185
|
disabled={isSimulating}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView, Platform, Modal, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, Modal, TouchableOpacity, ActivityIndicator } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -12,7 +12,7 @@ import { backVerification, checkTemplateType, frontVerification } from '../../mo
|
|
|
12
12
|
import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
|
|
13
13
|
import pathToBase64 from '../../utils/pathToBase64';
|
|
14
14
|
import kycService, { truncateFields } from '../../modules/api/KYCService';
|
|
15
|
-
import { cropByObb, cropImageWithBBox, cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
15
|
+
import { cropByObb, cropImageWithBBox, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
|
|
16
16
|
import { isMobileWeb } from '../../utils/deviceDetection';
|
|
17
17
|
import { logger } from '../../utils/logger';
|
|
18
18
|
|
|
@@ -252,6 +252,16 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
252
252
|
setSilentCaptureResult((prev) => ({ ...prev, templatePath: templatePath }));
|
|
253
253
|
}
|
|
254
254
|
if (templateType.card_obb) {
|
|
255
|
+
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
256
|
+
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
257
|
+
setSilentCaptureResult((prev) => ({
|
|
258
|
+
...prev,
|
|
259
|
+
isAnalyzing: false,
|
|
260
|
+
success: false,
|
|
261
|
+
error: t('kyc.idCardCapture.cardNotFullyInFrame'),
|
|
262
|
+
}));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
255
265
|
let bbox: IBbox | undefined;
|
|
256
266
|
try {
|
|
257
267
|
const crop = await cropByObb(result?.path || '', (templateType as any).card_obb);
|
|
@@ -314,8 +324,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
314
324
|
}).catch((e: any) => {
|
|
315
325
|
console.log("error front verification", e);
|
|
316
326
|
logger.log("error front verification", truncateFields(e));
|
|
317
|
-
|
|
318
|
-
|
|
327
|
+
const isCardNotFullyInFrame =
|
|
328
|
+
e?.message === 'CARD_NOT_FULLY_IN_FRAME' ||
|
|
329
|
+
e?.message?.includes('entirement') ||
|
|
330
|
+
e?.message?.includes('fully in frame');
|
|
331
|
+
const errorMessage = isCardNotFullyInFrame
|
|
332
|
+
? t('kyc.idCardCapture.cardNotFullyInFrame')
|
|
333
|
+
: (e?.message || 'Erreur de détection du MRZ');
|
|
334
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: errorMessage }));
|
|
319
335
|
});
|
|
320
336
|
} catch (error: any) {
|
|
321
337
|
console.log("Error setting up frontVerification call", error);
|
|
@@ -351,8 +367,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
351
367
|
}
|
|
352
368
|
}).catch((e: any) => {
|
|
353
369
|
logger.log("error back verification", truncateFields(e));
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
const isCardNotFullyInFrame =
|
|
371
|
+
e?.message === 'CARD_NOT_FULLY_IN_FRAME' ||
|
|
372
|
+
e?.message?.includes('entirement') ||
|
|
373
|
+
e?.message?.includes('fully in frame');
|
|
374
|
+
const errorMessage = isCardNotFullyInFrame
|
|
375
|
+
? t('kyc.idCardCapture.cardNotFullyInFrame')
|
|
376
|
+
: (e?.message || 'Erreur de détection du MRZ');
|
|
377
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: errorMessage }));
|
|
356
378
|
})
|
|
357
379
|
}
|
|
358
380
|
|
|
@@ -452,9 +474,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
452
474
|
if (!currentUrl.searchParams.has('kyc_id') && state.session.session_id) {
|
|
453
475
|
currentUrl.searchParams.set('kyc_id', state.session.session_id);
|
|
454
476
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
currentUrl.searchParams.set('
|
|
477
|
+
currentUrl.searchParams.set('component_index', String(state.currentComponentIndex));
|
|
478
|
+
if (countrySelectionData?.code) {
|
|
479
|
+
currentUrl.searchParams.set('country', countrySelectionData.code);
|
|
480
|
+
if (countrySelectionData.documentType) currentUrl.searchParams.set('document_type', countrySelectionData.documentType);
|
|
481
|
+
if (countrySelectionData.region) currentUrl.searchParams.set('region', countrySelectionData.region);
|
|
458
482
|
}
|
|
459
483
|
return `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(currentUrl.toString())}`;
|
|
460
484
|
} catch (error) {
|
|
@@ -469,6 +493,22 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
469
493
|
|
|
470
494
|
|
|
471
495
|
|
|
496
|
+
// En reprise sur un autre appareil: afficher un chargement tant que les données de session ne sont pas restaurées
|
|
497
|
+
const isResumingSession = Boolean(state.session.session_id && state.currentComponentIndex > 0);
|
|
498
|
+
const sessionDataRestored = state.session.sessionDataRestored !== false;
|
|
499
|
+
if (isResumingSession && !sessionDataRestored && (!countrySelectionData || !selectedDocumentType)) {
|
|
500
|
+
return (
|
|
501
|
+
<View style={styles.root}>
|
|
502
|
+
<View style={[styles.container, { justifyContent: 'center', alignItems: 'center' }]}>
|
|
503
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
504
|
+
<Text style={[styles.description, { marginTop: 16 }]}>
|
|
505
|
+
{state.currentLanguage === 'en' ? 'Loading your session...' : 'Chargement de votre session...'}
|
|
506
|
+
</Text>
|
|
507
|
+
</View>
|
|
508
|
+
</View>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
472
512
|
// Vérifier si les données sont disponibles, sinon afficher un message d'erreur
|
|
473
513
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
474
514
|
return (
|
|
@@ -506,6 +546,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({
|
|
|
506
546
|
showSwitchCamera={true}
|
|
507
547
|
onSilentCapture={handleSilentCapture}
|
|
508
548
|
silentCaptureResult={silentCaptureResult}
|
|
549
|
+
captureStabilizationDelayMs={3000}
|
|
509
550
|
enableFlash={cameraConfig.flashMode === 'auto' || cameraConfig.flashMode === 'on'}
|
|
510
551
|
overlayComponent={<IdCardOverlay
|
|
511
552
|
xMin={cameraConfig.overlay.bbox.xMin}
|
|
@@ -752,7 +793,7 @@ const styles = StyleSheet.create({
|
|
|
752
793
|
height: '100%',
|
|
753
794
|
},
|
|
754
795
|
previewContainer: {
|
|
755
|
-
width: '
|
|
796
|
+
width: '95%',
|
|
756
797
|
backgroundColor: 'white',
|
|
757
798
|
margin: 10,
|
|
758
799
|
borderRadius: 10,
|
|
@@ -150,7 +150,7 @@ export const WelcomeTemplate: React.FC<WelcomeTemplateProps> = ({
|
|
|
150
150
|
|
|
151
151
|
{/* Get Started Button */}
|
|
152
152
|
<Button
|
|
153
|
-
title={buttonText}
|
|
153
|
+
title={buttonText?.length === 0 ? t('kyc.welcome.getStarted') || 'Get Started' : buttonText}
|
|
154
154
|
fullWidth
|
|
155
155
|
onPress={handleGetStarted}
|
|
156
156
|
style={{ paddingVertical: 20, marginTop: 36 }}
|
|
@@ -175,6 +175,7 @@ const styles = StyleSheet.create({
|
|
|
175
175
|
shadowRadius: 1.84,
|
|
176
176
|
elevation: 3,
|
|
177
177
|
maxWidth: 760,
|
|
178
|
+
width: '94%',
|
|
178
179
|
},
|
|
179
180
|
title: {
|
|
180
181
|
fontSize: 24,
|
|
@@ -81,6 +81,8 @@ export interface EnhancedCameraViewProps {
|
|
|
81
81
|
onVideoRecordingStart?: () => void;
|
|
82
82
|
onVideoRecordingStop?: (result: { success: boolean; path?: string; error?: string }) => void;
|
|
83
83
|
videoDuration?: number;
|
|
84
|
+
/** Delay in ms before first auto-capture (lets camera focus and user position document). Recommended 2500–3000 for ID/document capture. */
|
|
85
|
+
captureStabilizationDelayMs?: number;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
export interface CheckTemplateTypeResponse {
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { View, SafeAreaView } from 'react-native';
|
|
3
3
|
import { TemplateKYCFlow } from './TemplateKYCFlowRefactored';
|
|
4
4
|
import { KYCTemplate, VerificationState } from '../types/KYC.types';
|
|
5
|
-
import { KycEnvironment } from '../types/env.types';
|
|
5
|
+
import { KycEnvironment, BackendEnvironment } from '../types/env.types';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
@@ -144,10 +144,12 @@ export const TemplateKYCExample: React.FC<{
|
|
|
144
144
|
API_KEY?: string;
|
|
145
145
|
templateId?: string;
|
|
146
146
|
template?: KYCTemplate;
|
|
147
|
-
env?: KycEnvironment;
|
|
147
|
+
env?: KycEnvironment; // Flow execution: PRODUCTION (full AI) or SANDBOX (skip AI)
|
|
148
|
+
serverEnv?: BackendEnvironment; // Backend to call: PRODUCTION or TEST (API URL)
|
|
148
149
|
existingSessionId?: string;
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
initialComponentIndex?: number;
|
|
151
|
+
initialCountryResume?: { code: string; documentType: string; region?: string };
|
|
152
|
+
}> = ({ onComplete, onCancel, onError, language, API_KEY, templateId, template, env = 'PRODUCTION', serverEnv, existingSessionId, initialComponentIndex, initialCountryResume }) => {
|
|
151
153
|
const handleComplete = (data: VerificationState) => {
|
|
152
154
|
console.log('KYC Template completed with data:', data);
|
|
153
155
|
onComplete(data);
|
|
@@ -180,8 +182,10 @@ export const TemplateKYCExample: React.FC<{
|
|
|
180
182
|
language={language} // ou "en" pour l'anglais
|
|
181
183
|
API_KEY={API_KEY}
|
|
182
184
|
env={env}
|
|
185
|
+
serverEnv={serverEnv}
|
|
183
186
|
existingSessionId={existingSessionId}
|
|
184
|
-
|
|
187
|
+
initialComponentIndex={initialComponentIndex}
|
|
188
|
+
initialCountryResume={initialCountryResume}
|
|
185
189
|
/>
|
|
186
190
|
</View>
|
|
187
191
|
</SafeAreaView>
|
|
@@ -18,7 +18,8 @@ import { EmailVerificationTemplate } from './KYCElements/EmailVerificationTempla
|
|
|
18
18
|
import { PhoneVerificationTemplate } from './KYCElements/PhoneVerificationTemplate';
|
|
19
19
|
import { PersonalInformationTemplate } from './KYCElements/PersonalInformationTemplate';
|
|
20
20
|
import { AdditionalDocumentsTemplate } from './KYCElements/AdditionalDocumentsTemplate';
|
|
21
|
-
import { KycEnvironment } from '../types/env.types';
|
|
21
|
+
import { KycEnvironment, BackendEnvironment } from '../types/env.types';
|
|
22
|
+
import KYCConfig from '../config/KYCConfig';
|
|
22
23
|
|
|
23
24
|
interface TemplateKYCFlowProps {
|
|
24
25
|
template?: KYCTemplate; // Format SDK direct (existing, now optional)
|
|
@@ -28,9 +29,13 @@ interface TemplateKYCFlowProps {
|
|
|
28
29
|
language?: string;
|
|
29
30
|
onCancel?: () => void;
|
|
30
31
|
API_KEY?: string; // Required if templateId is used
|
|
31
|
-
env?: KycEnvironment; //
|
|
32
|
+
env?: KycEnvironment; // Flow execution: PRODUCTION (full AI) or SANDBOX (skip AI)
|
|
33
|
+
serverEnv?: BackendEnvironment; // Backend to call: PRODUCTION or TEST (API URL)
|
|
32
34
|
existingSessionId?: string;
|
|
33
|
-
|
|
35
|
+
/** Index in template.components (0-based) to resume at — e.g. from URL component_index or template table */
|
|
36
|
+
initialComponentIndex?: number;
|
|
37
|
+
/** Pays / type de document depuis l'URL de reprise (reprise multi-appareil) */
|
|
38
|
+
initialCountryResume?: { code: string; documentType: string; region?: string };
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
|
|
@@ -42,12 +47,21 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
|
|
|
42
47
|
onCancel,
|
|
43
48
|
API_KEY,
|
|
44
49
|
env = 'PRODUCTION',
|
|
50
|
+
serverEnv,
|
|
45
51
|
existingSessionId,
|
|
46
|
-
|
|
52
|
+
initialComponentIndex,
|
|
53
|
+
initialCountryResume,
|
|
47
54
|
}) => {
|
|
48
55
|
const { t } = useI18n();
|
|
49
56
|
const { template: loadedTemplate, isLoading, error, loadTemplate } = useTemplateLoader();
|
|
50
57
|
|
|
58
|
+
// Which backend URL to call (independent from flow env)
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (serverEnv !== undefined) {
|
|
61
|
+
KYCConfig.setBackendEnvironment(serverEnv);
|
|
62
|
+
}
|
|
63
|
+
}, [serverEnv]);
|
|
64
|
+
|
|
51
65
|
// Validate props
|
|
52
66
|
useEffect(() => {
|
|
53
67
|
if (!providedTemplate && !templateId) {
|
|
@@ -136,7 +150,8 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
|
|
|
136
150
|
apiKey={API_KEY}
|
|
137
151
|
env={env}
|
|
138
152
|
existingSessionId={existingSessionId}
|
|
139
|
-
|
|
153
|
+
initialComponentIndex={initialComponentIndex}
|
|
154
|
+
initialCountryResume={initialCountryResume}
|
|
140
155
|
>
|
|
141
156
|
<TemplateKYCFlowContent onCancel={OnCancel} />
|
|
142
157
|
</TemplateKYCFlowProvider>
|
|
@@ -241,7 +256,10 @@ const TemplateKYCFlowContent: React.FC<{ onCancel?: () => void }> = ({ onCancel
|
|
|
241
256
|
{(state.showCustomStepper && state.session.isInitialized) ? (
|
|
242
257
|
<View style={styles.header}>
|
|
243
258
|
<Text style={styles.progressText}>
|
|
244
|
-
{t('kyc.step', {
|
|
259
|
+
{t('kyc.step', {
|
|
260
|
+
current: (state.template.components.findIndex(c => c.id === currentComponent.id) + 1) || (state.currentComponentIndex + 1),
|
|
261
|
+
total: state.template.components.length,
|
|
262
|
+
})}
|
|
245
263
|
</Text>
|
|
246
264
|
<View style={styles.progressContainer}>
|
|
247
265
|
<View style={styles.progressBar}>
|
package/src/config/KYCConfig.ts
CHANGED
|
@@ -6,7 +6,7 @@ class KYCConfig {
|
|
|
6
6
|
|
|
7
7
|
private backendUrls: Record<BackendEnvironment, string> = {
|
|
8
8
|
PRODUCTION: 'https://service.sanctumkey.com/api/v1',
|
|
9
|
-
TEST: 'https://
|
|
9
|
+
TEST: 'https://kyc-backend.transfergratis.net/api/v1', // Placeholder URL
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
private constructor() { }
|