@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.
Files changed (88) hide show
  1. package/android/src/main/AndroidManifest.xml +12 -0
  2. package/build/components/EnhancedCameraView.web.d.ts.map +1 -1
  3. package/build/components/EnhancedCameraView.web.js +76 -21
  4. package/build/components/EnhancedCameraView.web.js.map +1 -1
  5. package/build/components/KYCElements/EmailVerificationTemplate.d.ts.map +1 -1
  6. package/build/components/KYCElements/EmailVerificationTemplate.js +48 -29
  7. package/build/components/KYCElements/EmailVerificationTemplate.js.map +1 -1
  8. package/build/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  9. package/build/components/KYCElements/IDCardCapture.js +48 -11
  10. package/build/components/KYCElements/IDCardCapture.js.map +1 -1
  11. package/build/components/KYCElements/WelcomeTemplate.js +2 -1
  12. package/build/components/KYCElements/WelcomeTemplate.js.map +1 -1
  13. package/build/components/OverLay/type.d.ts +2 -0
  14. package/build/components/OverLay/type.d.ts.map +1 -1
  15. package/build/components/OverLay/type.js.map +1 -1
  16. package/build/components/TemplateKYCExample.d.ts +8 -2
  17. package/build/components/TemplateKYCExample.d.ts.map +1 -1
  18. package/build/components/TemplateKYCExample.js +2 -2
  19. package/build/components/TemplateKYCExample.js.map +1 -1
  20. package/build/components/TemplateKYCFlowRefactored.d.ts +10 -2
  21. package/build/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  22. package/build/components/TemplateKYCFlowRefactored.js +13 -3
  23. package/build/components/TemplateKYCFlowRefactored.js.map +1 -1
  24. package/build/config/KYCConfig.js +1 -1
  25. package/build/config/KYCConfig.js.map +1 -1
  26. package/build/hooks/useTemplateKYCFlow.d.ts +14 -2
  27. package/build/hooks/useTemplateKYCFlow.d.ts.map +1 -1
  28. package/build/hooks/useTemplateKYCFlow.js +175 -84
  29. package/build/hooks/useTemplateKYCFlow.js.map +1 -1
  30. package/build/i18n/en/index.d.ts +2 -0
  31. package/build/i18n/en/index.d.ts.map +1 -1
  32. package/build/i18n/en/index.js +3 -1
  33. package/build/i18n/en/index.js.map +1 -1
  34. package/build/i18n/fr/index.d.ts +2 -0
  35. package/build/i18n/fr/index.d.ts.map +1 -1
  36. package/build/i18n/fr/index.js +3 -1
  37. package/build/i18n/fr/index.js.map +1 -1
  38. package/build/i18n/types.d.ts +2 -0
  39. package/build/i18n/types.d.ts.map +1 -1
  40. package/build/i18n/types.js.map +1 -1
  41. package/build/modules/api/CardAuthentification.d.ts.map +1 -1
  42. package/build/modules/api/CardAuthentification.js +28 -2
  43. package/build/modules/api/CardAuthentification.js.map +1 -1
  44. package/build/modules/api/KYCService.d.ts +10 -0
  45. package/build/modules/api/KYCService.d.ts.map +1 -1
  46. package/build/modules/api/KYCService.js +24 -0
  47. package/build/modules/api/KYCService.js.map +1 -1
  48. package/build/modules/camera/VisionCameraModule.web.d.ts.map +1 -1
  49. package/build/modules/camera/VisionCameraModule.web.js +27 -8
  50. package/build/modules/camera/VisionCameraModule.web.js.map +1 -1
  51. package/build/types/KYC.types.d.ts +6 -2
  52. package/build/types/KYC.types.d.ts.map +1 -1
  53. package/build/types/KYC.types.js.map +1 -1
  54. package/build/utils/cropByObb.d.ts +17 -0
  55. package/build/utils/cropByObb.d.ts.map +1 -1
  56. package/build/utils/cropByObb.js +51 -1
  57. package/build/utils/cropByObb.js.map +1 -1
  58. package/build/web/WebKYCEntry.d.ts.map +1 -1
  59. package/build/web/WebKYCEntry.js +11 -5
  60. package/build/web/WebKYCEntry.js.map +1 -1
  61. package/package.json +1 -1
  62. package/plugin/build/index.d.ts +1 -0
  63. package/plugin/build/index.js +3 -1
  64. package/plugin/build/withRemovePermissions.d.ts +3 -0
  65. package/plugin/build/withRemovePermissions.js +67 -0
  66. package/plugin/src/index.ts +2 -1
  67. package/plugin/src/withRemovePermissions.js +85 -0
  68. package/plugin/src/withRemovePermissions.ts +83 -0
  69. package/plugin/tsconfig.tsbuildinfo +1 -1
  70. package/plugin.js +6 -1
  71. package/src/components/EnhancedCameraView.web.tsx +76 -21
  72. package/src/components/KYCElements/EmailVerificationTemplate.tsx +47 -33
  73. package/src/components/KYCElements/IDCardCapture.tsx +51 -10
  74. package/src/components/KYCElements/WelcomeTemplate.tsx +2 -1
  75. package/src/components/OverLay/type.ts +2 -0
  76. package/src/components/TemplateKYCExample.tsx +9 -5
  77. package/src/components/TemplateKYCFlowRefactored.tsx +24 -6
  78. package/src/config/KYCConfig.ts +1 -1
  79. package/src/hooks/useTemplateKYCFlow.tsx +189 -95
  80. package/src/i18n/en/index.ts +3 -1
  81. package/src/i18n/fr/index.ts +3 -1
  82. package/src/i18n/types.ts +2 -0
  83. package/src/modules/api/CardAuthentification.ts +30 -2
  84. package/src/modules/api/KYCService.ts +41 -0
  85. package/src/modules/camera/VisionCameraModule.web.ts +30 -12
  86. package/src/types/KYC.types.ts +7 -3
  87. package/src/utils/cropByObb.ts +57 -1
  88. 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
- // Test camera permission
74
- const stream = await navigator.mediaDevices.getUserMedia({ video: true });
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
- const newStream = await navigator.mediaDevices.getUserMedia(constraints);
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
- // Draw the current video frame to canvas
163
- context.drawImage(video, 0, 0);
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.8);
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
- // Automatically take a silent photo every 5 seconds when ready
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 intervalId = setInterval(() => {
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
- }, 5000);
240
+ intervalId = setInterval(captureSilentPhoto, intervalMs);
241
+ }, delayMs);
191
242
 
192
- return () => clearInterval(intervalId);
193
- }, [showCamera, isInitialized, captureSilentPhoto]);
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
- if (!email || !email.includes('@')) {
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
- // Simulate API call to send code
53
- setTimeout(() => {
54
- setIsSimulating(false);
61
+ try {
62
+ await kycService.sendEmailVerificationCode(trimmed, auth);
55
63
  setStep('otp');
56
- // For demo purposes, we could show an alert or toast here with the code
57
- // But we'll just expect them to know 123456 or type anything for now if not strict
58
- }, 1500);
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
- // Simulate verification API
71
- setTimeout(() => {
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
- // Resend logic
165
- Alert.alert(
166
- t('common.codeResent') || 'Code Resent',
167
- t('common.codeResentMessage', { email }) || 'Code resent to ' + email
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
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: e?.message || 'Erreur de détection du MRZ' }));
318
- // showAlert('Erreur', e?.message || 'Erreur de détection du MRZ');
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
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, templatePath: templatePath, success: false, error: e?.message || 'Erreur de détection du MRZ' }));
355
- // showAlert('Erreur', e?.message || 'Erreur de détection du MRZ');
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
- // Ajouter l'étape actuelle pour permettre à l'utilisateur de continuer au bon endroit
456
- if (!currentUrl.searchParams.has('step')) {
457
- currentUrl.searchParams.set('step', String(state.currentComponentIndex));
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: '100%',
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
- initialStep?: number;
150
- }> = ({ onComplete, onCancel, onError, language, API_KEY, templateId, template, env = 'PRODUCTION', existingSessionId, initialStep }) => {
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
- initialStep={initialStep}
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; // Environment mode: PRODUCTION or SANDBOX
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
- initialStep?: number; // Initial step index to resume verification
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
- initialStep,
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
- initialStep={initialStep}
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', { current: state.currentComponentIndex + 1, total: state.template.components.length })}
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}>
@@ -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://test-service.sanctumkey.com/api/v1', // Placeholder URL
9
+ TEST: 'https://kyc-backend.transfergratis.net/api/v1', // Placeholder URL
10
10
  };
11
11
 
12
12
  private constructor() { }