@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
@@ -4,6 +4,8 @@ import { KycEnvironment } from '../types/env.types';
4
4
  import kycService, { authentification, truncateFields } from '../modules/api/KYCService';
5
5
  import useI18n from './useI18n';
6
6
  import { logger } from '../utils/logger';
7
+ import { countryMapping } from '../config/region_mapping';
8
+ import { countryData } from '../config/countriesData';
7
9
 
8
10
  // Context pour le provider
9
11
  interface TemplateKYCFlowContextType {
@@ -17,6 +19,7 @@ interface TemplateKYCFlowContextType {
17
19
  getLocalizedText: (text: { en: string; fr: string;[key: string]: string }) => string;
18
20
  initializeSession: () => Promise<void>;
19
21
  env: KycEnvironment;
22
+ apiKey?: string;
20
23
  }
21
24
 
22
25
  const TemplateKYCFlowContext = createContext<TemplateKYCFlowContextType | undefined>(undefined);
@@ -32,7 +35,10 @@ interface TemplateKYCFlowProviderProps {
32
35
  apiKey?: string;
33
36
  env?: KycEnvironment;
34
37
  existingSessionId?: string;
35
- initialStep?: number;
38
+ /** Index in template.components where to resume (0-based). Simple to store in template table. */
39
+ initialComponentIndex?: number;
40
+ /** Pays / type de document depuis l'URL de reprise — évite de dépendre du backend pour afficher les pays. */
41
+ initialCountryResume?: { code: string; documentType: string; region?: string };
36
42
  }
37
43
 
38
44
  export const TemplateKYCFlowProvider: React.FC<TemplateKYCFlowProviderProps> = ({
@@ -45,8 +51,10 @@ export const TemplateKYCFlowProvider: React.FC<TemplateKYCFlowProviderProps> = (
45
51
  apiKey,
46
52
  env = 'PRODUCTION',
47
53
  existingSessionId,
54
+ initialComponentIndex,
55
+ initialCountryResume,
48
56
  }) => {
49
- const hookResult = useTemplateKYCFlow(template, onComplete, onError, onCancel, initialLanguage, apiKey, env, existingSessionId);
57
+ const hookResult = useTemplateKYCFlow(template, onComplete, onError, onCancel, initialLanguage, apiKey, env, existingSessionId, initialComponentIndex, initialCountryResume);
50
58
 
51
59
  return (
52
60
  <TemplateKYCFlowContext.Provider value={hookResult}>
@@ -73,7 +81,8 @@ export const useTemplateKYCFlow = (
73
81
  apiKey?: string,
74
82
  env: KycEnvironment = 'PRODUCTION',
75
83
  existingSessionId?: string,
76
- initialStep?: number,
84
+ initialComponentIndex?: number,
85
+ initialCountryResume?: { code: string; documentType: string; region?: string },
77
86
  ): UseTemplateReturn => {
78
87
 
79
88
  const { setLocale } = useI18n();
@@ -140,92 +149,90 @@ export const useTemplateKYCFlow = (
140
149
  const templateWithReview = useMemo(() => ensureReviewSubmitStep(template), [template, ensureReviewSubmitStep, apiKey]);
141
150
  const templateWithReviewAndVerification = useMemo(() => ensureVerificationProgressStep(templateWithReview), [templateWithReview, ensureVerificationProgressStep, apiKey]);
142
151
 
143
- // État initial du flux
152
+ // État initial du flux (initialComponentIndex = index dans template.components pour reprendre au bon composant)
144
153
  const buildInitialState = (): TemplateState => {
145
- // Valider initialStep pour s'assurer qu'il est dans les limites du template
146
- let validInitialStep = 0;
154
+ let resumeAtIndex = 0;
147
155
  let completedComponents: number[] = [];
148
-
149
- logger.log('buildInitialState called', { initialStep, existingSessionId });
150
-
151
- if (initialStep !== undefined && initialStep >= 0) {
156
+ let initialComponentData: Record<number, unknown> = {};
157
+
158
+ logger.log('buildInitialState called', { initialComponentIndex, existingSessionId, initialCountryResume });
159
+
160
+ if (initialComponentIndex !== undefined && initialComponentIndex >= 0) {
152
161
  const maxIndex = templateWithReviewAndVerification.components.length - 1;
153
- const requestedStep = Math.min(initialStep, maxIndex);
154
- const requestedComponent = templateWithReviewAndVerification.components[requestedStep];
155
-
156
- logger.log('Processing initialStep', {
157
- initialStep,
158
- requestedStep,
162
+ const requestedIndex = Math.min(initialComponentIndex, maxIndex);
163
+ const requestedComponent = templateWithReviewAndVerification.components[requestedIndex];
164
+
165
+ logger.log('Processing initialComponentIndex (component in template)', {
166
+ initialComponentIndex,
167
+ requestedIndex,
159
168
  maxIndex,
160
169
  componentType: requestedComponent?.type,
161
- componentId: requestedComponent?.id
170
+ componentId: requestedComponent?.id,
162
171
  });
163
-
164
- // Si on reprend à l'étape id_card, on peut rester à id_card si on a une session existante
165
- // car les données de country_selection peuvent être chargées depuis la session
172
+
173
+ // Reprendre au composant en cours (id_card, selfie, etc.)
166
174
  if (requestedComponent?.type === 'id_card') {
167
- // Si on a une session existante, on peut rester à id_card et charger les données
168
175
  if (existingSessionId) {
169
176
  logger.log('id_card with existing session - staying at id_card');
170
- validInitialStep = requestedStep;
171
- // Marquer les composants précédents comme complétés
172
- if (validInitialStep > 0) {
177
+ resumeAtIndex = requestedIndex;
178
+ if (resumeAtIndex > 0) {
173
179
  completedComponents = templateWithReviewAndVerification.components
174
- .slice(0, validInitialStep)
180
+ .slice(0, resumeAtIndex)
175
181
  .map(component => component.id);
176
182
  }
177
183
  } else {
178
- // Si pas de session, revenir à country_selection pour refaire le choix
179
184
  const countrySelectionIndex = templateWithReviewAndVerification.components.findIndex(
180
185
  c => c.type === 'country_selection'
181
186
  );
182
-
183
187
  logger.log('id_card without session - going back to country_selection', { countrySelectionIndex });
184
-
185
188
  if (countrySelectionIndex >= 0) {
186
- validInitialStep = countrySelectionIndex;
187
- // Marquer les composants avant country_selection comme complétés
189
+ resumeAtIndex = countrySelectionIndex;
188
190
  if (countrySelectionIndex > 0) {
189
191
  completedComponents = templateWithReviewAndVerification.components
190
192
  .slice(0, countrySelectionIndex)
191
193
  .map(component => component.id);
192
194
  }
193
195
  } else {
194
- // Si pas de country_selection, commencer au début
195
- validInitialStep = 0;
196
+ resumeAtIndex = 0;
196
197
  }
197
198
  }
198
199
  } else if (requestedComponent?.type === 'review_submit') {
199
- // Si on reprend au review_submit, on ne marque pas les composants précédents
200
- // pour permettre à l'utilisateur de revenir en arrière et vérifier/modifier les données
201
- validInitialStep = requestedStep;
202
- // Ne pas marquer les composants précédents comme complétés
200
+ resumeAtIndex = requestedIndex;
203
201
  completedComponents = [];
204
202
  } else {
205
- // Pour les autres composants (selfie, etc.), commencer directement à l'étape demandée
206
- validInitialStep = requestedStep;
207
-
208
- // Marquer tous les composants précédents comme complétés
209
- // Cela permet à l'utilisateur de continuer sans refaire les étapes précédentes
210
- if (validInitialStep > 0) {
203
+ resumeAtIndex = requestedIndex;
204
+ if (resumeAtIndex > 0) {
211
205
  completedComponents = templateWithReviewAndVerification.components
212
- .slice(0, validInitialStep)
206
+ .slice(0, resumeAtIndex)
213
207
  .map(component => component.id);
214
208
  }
215
209
  }
216
-
217
- logger.log('Final initial state', {
218
- validInitialStep,
210
+
211
+ logger.log('Final initial state (resume at component index)', {
212
+ resumeAtIndex,
219
213
  completedComponentsCount: completedComponents.length,
220
- componentAtStep: templateWithReviewAndVerification.components[validInitialStep]?.type
214
+ componentAtResume: templateWithReviewAndVerification.components[resumeAtIndex]?.type,
221
215
  });
222
216
  }
223
-
217
+
218
+ if (initialCountryResume?.code && initialCountryResume?.documentType) {
219
+ const countrySel = templateWithReviewAndVerification.components.find(c => c.type === 'country_selection');
220
+ if (countrySel && countryData[initialCountryResume.code]) {
221
+ initialComponentData[countrySel.id] = {
222
+ code: initialCountryResume.code,
223
+ documentType: initialCountryResume.documentType,
224
+ region: initialCountryResume.region || 'root',
225
+ ...countryData[initialCountryResume.code],
226
+ };
227
+ logger.log('Prefilled country_selection from URL', { code: initialCountryResume.code, documentType: initialCountryResume.documentType });
228
+ }
229
+ }
230
+
224
231
  return {
225
232
  template: templateWithReviewAndVerification,
226
- currentComponentIndex: validInitialStep,
233
+ currentComponentIndex: resumeAtIndex,
227
234
  completedComponents: completedComponents,
228
- componentData: {},
235
+ componentData: initialComponentData,
229
236
  errors: {},
230
237
  isProcessing: false,
231
238
  currentLanguage: initialLanguage,
@@ -236,6 +243,7 @@ export const useTemplateKYCFlow = (
236
243
  isInitialized: false,
237
244
  isProcessing: false,
238
245
  error: null,
246
+ sessionDataRestored: !existingSessionId || Boolean(initialCountryResume?.code && initialCountryResume?.documentType),
239
247
  },
240
248
  verification: {
241
249
  status: 'idle',
@@ -265,9 +273,9 @@ export const useTemplateKYCFlow = (
265
273
  return;
266
274
  }
267
275
 
268
- // Si initialStep n'est pas défini ou est 0, on ne charge pas (début de session)
269
- if (initialStep === undefined || initialStep === 0) {
270
- logger.log('initialStep is 0 or undefined, skipping data load');
276
+ // Si initialComponentIndex n'est pas défini ou est 0, on ne charge pas (début de session)
277
+ if (initialComponentIndex === undefined || initialComponentIndex === 0) {
278
+ logger.log('initialComponentIndex is 0 or undefined, skipping data load');
271
279
  return;
272
280
  }
273
281
 
@@ -281,7 +289,7 @@ export const useTemplateKYCFlow = (
281
289
  }
282
290
 
283
291
  try {
284
- logger.log('Loading session data for resume:', { sessionId: existingSessionId, step: initialStep });
292
+ logger.log('Loading session data for resume:', { sessionId: existingSessionId, componentIndex: initialComponentIndex });
285
293
  const result = await kycService.getVerificationResult(state.session.session_id);
286
294
  const sessionData = result[state.session.session_id]?.data;
287
295
 
@@ -291,10 +299,9 @@ export const useTemplateKYCFlow = (
291
299
  const data: any = sessionData;
292
300
  const restoredComponentData: Record<number, any> = {};
293
301
 
294
- // Parcourir les composants jusqu'à l'étape initiale (incluse) pour restaurer leurs données
295
- // Utiliser initialStep + 1 pour inclure le composant à l'étape initialStep
302
+ // Parcourir les composants jusqu'au composant de reprise (inclu) pour restaurer leurs données
296
303
  templateWithReviewAndVerification.components
297
- .slice(0, initialStep + 1)
304
+ .slice(0, initialComponentIndex + 1)
298
305
  .forEach((component) => {
299
306
  // Essayer de restaurer les données selon le type de composant
300
307
  if (component.type === 'id_card' || component.type === 'file_upload') {
@@ -378,8 +385,27 @@ export const useTemplateKYCFlow = (
378
385
  }
379
386
  }
380
387
  } else if (component.type === 'country_selection') {
381
- // Les données de sélection de pays peuvent être dans metadata
382
- if (data.metadata || data.user_data) {
388
+ // Reconstruire country_selection au format attendu par IDCardCapture / CountrySelectionTemplate (code, documentType, region, regionMapping)
389
+ const meta = data.metadata || data.user_data || data;
390
+ const code = meta.country || meta.country_code || meta.code;
391
+ const documentType = meta.document_type || meta.documentType;
392
+ const region = meta.region;
393
+ if (code && documentType != null) {
394
+ const country = countryData[code];
395
+ const mapping = countryMapping[code as keyof typeof countryMapping];
396
+ if (country) {
397
+ restoredComponentData[component.id] = {
398
+ code,
399
+ ...country,
400
+ documentType,
401
+ region: region || undefined,
402
+ regionMapping: mapping || undefined,
403
+ };
404
+ logger.log('Restored country_selection for resume', { code, documentType, region });
405
+ } else {
406
+ restoredComponentData[component.id] = { code, documentType, region: region || undefined, regionMapping: mapping || undefined };
407
+ }
408
+ } else if (data.metadata || data.user_data) {
383
409
  restoredComponentData[component.id] = {
384
410
  ...(data.metadata || {}),
385
411
  ...(data.user_data || {}),
@@ -396,6 +422,30 @@ export const useTemplateKYCFlow = (
396
422
  }
397
423
  });
398
424
 
425
+ // Fallback: si pas de country_selection restauré mais on a des documents avec templatePath, déduire code/documentType (ex. "templates/national_id_CM_front.jpg")
426
+ const countrySelectionComponent = templateWithReviewAndVerification.components.find(c => c.type === 'country_selection');
427
+ if (countrySelectionComponent && !restoredComponentData[countrySelectionComponent.id] && data.documents) {
428
+ const docs = data.documents as Record<string, { templatePath?: string }>;
429
+ const firstDoc = Object.values(docs).find(d => d?.templatePath);
430
+ const path = firstDoc?.templatePath || '';
431
+ const match = path.match(/([a-z_]+)_([A-Z]{2})(?:_|$)/i) || path.match(/([A-Z]{2})/);
432
+ const code = match ? (match[2] || match[1]).toUpperCase().slice(0, 2) : null;
433
+ const docTypeFromPath = path.match(/(national_id|identity_card|passport|passport_card)/i)?.[1]?.toLowerCase().replace('identity_card', 'national_id') || null;
434
+ if (code && countryData[code]) {
435
+ const documentType = docTypeFromPath || 'national_id';
436
+ const mapping = countryMapping[code as keyof typeof countryMapping];
437
+ const country = countryData[code];
438
+ restoredComponentData[countrySelectionComponent.id] = {
439
+ code,
440
+ ...country,
441
+ documentType,
442
+ region: undefined,
443
+ regionMapping: mapping || undefined,
444
+ };
445
+ logger.log('Restored country_selection from document templatePath', { code, documentType, path });
446
+ }
447
+ }
448
+
399
449
  // Mettre à jour l'état avec les données restaurées
400
450
  if (Object.keys(restoredComponentData).length > 0) {
401
451
  logger.log('Session data restored - components:', Object.keys(restoredComponentData));
@@ -406,20 +456,40 @@ export const useTemplateKYCFlow = (
406
456
  ...prev.componentData,
407
457
  ...restoredComponentData,
408
458
  },
459
+ session: { ...prev.session, sessionDataRestored: true },
409
460
  }));
410
461
  logger.log('Component data updated in state');
411
462
  } else {
412
463
  logger.log('No component data to restore from session');
464
+ setState(prev => ({ ...prev, session: { ...prev.session, sessionDataRestored: true } }));
413
465
  }
466
+ } else {
467
+ setState(prev => ({ ...prev, session: { ...prev.session, sessionDataRestored: true } }));
414
468
  }
415
469
  } catch (error) {
416
470
  logger.error('Error loading session data:', truncateFields(error));
417
- // Ne pas bloquer le flux si le chargement échoue
471
+ setState(prev => ({ ...prev, session: { ...prev.session, sessionDataRestored: true } }));
418
472
  }
419
473
  };
420
474
 
421
475
  loadSessionData();
422
- }, [existingSessionId, initialStep, state.session.isInitialized, state.session.session_id, templateWithReviewAndVerification.components, base64ToDataUri]);
476
+ }, [existingSessionId, initialComponentIndex, state.session.isInitialized, state.session.session_id, templateWithReviewAndVerification.components, base64ToDataUri]);
477
+
478
+ // Si l'index pointe vers Review alors que des étapes ne sont pas complétées, ramener à la première étape incomplète
479
+ useEffect(() => {
480
+ const comp = state.template.components[state.currentComponentIndex];
481
+ if (!comp || comp.type !== 'review_submit') return;
482
+ const nonReview = state.template.components.filter(
483
+ c => c.type !== 'review_submit' && c.type !== 'verification_progress'
484
+ );
485
+ if (nonReview.every(c => state.completedComponents.includes(c.id))) return;
486
+ const firstIncomplete = nonReview.find(c => !state.completedComponents.includes(c.id));
487
+ if (!firstIncomplete) return;
488
+ const targetIndex = state.template.components.findIndex(c => c.id === firstIncomplete.id);
489
+ if (targetIndex >= 0 && targetIndex !== state.currentComponentIndex) {
490
+ setState(prev => ({ ...prev, currentComponentIndex: targetIndex }));
491
+ }
492
+ }, [state.currentComponentIndex, state.completedComponents, state.template.components]);
423
493
 
424
494
  const mapComponentTypeToAction = useCallback((type: TemplateComponent['type']): string | null => {
425
495
  switch (type) {
@@ -475,7 +545,7 @@ export const useTemplateKYCFlow = (
475
545
  if (!action) {
476
546
  return base;
477
547
  }
478
- // Document upload expects an array of documents with base64 and metadata
548
+ // Document upload expects documents; include country_selection in metadata so resume can restore it
479
549
  if (action === 'document_upload') {
480
550
  const documents: Record<string, any> = {};
481
551
  if (rawData && typeof rawData === 'object') {
@@ -483,9 +553,18 @@ export const useTemplateKYCFlow = (
483
553
  documents[key] = rawData[key];
484
554
  });
485
555
  }
556
+ const countryComp = state.template.components.find(c => c.type === 'country_selection');
557
+ const countryDataForPayload = countryComp ? state.componentData[countryComp.id] : null;
558
+ const metadata: Record<string, unknown> = {};
559
+ if (countryDataForPayload?.code) {
560
+ metadata.country = countryDataForPayload.code;
561
+ metadata.document_type = countryDataForPayload.documentType;
562
+ if (countryDataForPayload.region != null) metadata.region = countryDataForPayload.region;
563
+ }
486
564
  return {
487
565
  ...base,
488
566
  documents,
567
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
489
568
  };
490
569
  }
491
570
 
@@ -520,17 +599,29 @@ export const useTemplateKYCFlow = (
520
599
  // console.log('apiKey in useTemplateKYCFlow', apiKey);
521
600
 
522
601
 
523
- // Composant actuel
602
+ // Composant actuel (peut être redirigé si on pointe vers Review alors que le flux n'est pas terminé)
524
603
  const currentComponent = useMemo(() => {
525
- return state.template.components[state.currentComponentIndex] || null;
526
- }, [state.template.components, state.currentComponentIndex, apiKey]);
527
-
528
- // Progression du flux
604
+ const comp = state.template.components[state.currentComponentIndex] || null;
605
+ if (!comp || comp.type !== 'review_submit') return comp;
606
+ const nonReview = state.template.components.filter(
607
+ c => c.type !== 'review_submit' && c.type !== 'verification_progress'
608
+ );
609
+ const allDone = nonReview.every(c => state.completedComponents.includes(c.id));
610
+ if (allDone) return comp;
611
+ const firstIncomplete = nonReview.find(c => !state.completedComponents.includes(c.id));
612
+ return firstIncomplete || comp;
613
+ }, [state.template.components, state.currentComponentIndex, state.completedComponents, apiKey]);
614
+
615
+ // Progression du flux (basée sur le composant effectivement affiché)
529
616
  const progress = useMemo(() => {
617
+ const idx = currentComponent
618
+ ? state.template.components.findIndex(c => c.id === currentComponent.id)
619
+ : state.currentComponentIndex;
620
+ const i = idx >= 0 ? idx : state.currentComponentIndex;
530
621
  return state.template.components.length > 0
531
- ? ((state.currentComponentIndex + 1) / state.template.components.length) * 100
622
+ ? ((i + 1) / state.template.components.length) * 100
532
623
  : 0;
533
- }, [state.currentComponentIndex, state.template.components.length, apiKey]);
624
+ }, [currentComponent, state.template.components, state.currentComponentIndex, apiKey]);
534
625
 
535
626
  // Vérifications de navigation
536
627
  const canGoNext = useMemo(() => {
@@ -623,12 +714,21 @@ export const useTemplateKYCFlow = (
623
714
  }));
624
715
  }
625
716
  }, [apiKey]);
626
- // Validation d'un composant
627
- const validateComponent = useCallback((componentId: number): boolean => {
717
+
718
+ // When user switches device: resume with existingSessionId + initialStep — ensure session is initialized so loadSessionData runs
719
+ useEffect(() => {
720
+ if (!existingSessionId || state.session.isInitialized) return;
721
+ if (state.session.session_id !== existingSessionId) return;
722
+ logger.log('Resuming on new device: initializing session so data can load', { existingSessionId, initialComponentIndex });
723
+ initializeSession();
724
+ }, [existingSessionId, state.session.session_id, state.session.isInitialized, initializeSession]);
725
+
726
+ // Validation d'un composant (dataOverride permet de valider sans attendre la mise à jour du state)
727
+ const validateComponent = useCallback((componentId: number, dataOverride?: any): boolean => {
628
728
  const component = state.template.components.find(c => c.id === componentId);
629
729
  if (!component) return false;
630
730
 
631
- const componentData = state.componentData[componentId];
731
+ const componentData = dataOverride !== undefined ? dataOverride : state.componentData[componentId];
632
732
 
633
733
  switch (component.type) {
634
734
  case 'id_card':
@@ -698,8 +798,8 @@ export const useTemplateKYCFlow = (
698
798
  }));
699
799
  }, [ensureReviewSubmitStep, ensureVerificationProgressStep, apiKey]),
700
800
 
701
- // Passer au composant suivant
702
- nextComponent: useCallback(async () => {
801
+ // Passer au composant suivant (overrideData = données du step courant si déjà connues, évite un double clic)
802
+ nextComponent: useCallback(async (overrideData?: any) => {
703
803
  if (!canGoNext) return;
704
804
 
705
805
  const currentComp = state.template.components[state.currentComponentIndex];
@@ -715,8 +815,8 @@ export const useTemplateKYCFlow = (
715
815
  isProcessing: true,
716
816
  };
717
817
  });
718
- // Valider le composant actuel
719
- if (!validateComponent(currentComp.id)) {
818
+ // Valider avec override ou state
819
+ if (!validateComponent(currentComp.id, overrideData)) {
720
820
  setState(prev => ({
721
821
  ...prev,
722
822
  isProcessing: false,
@@ -738,18 +838,16 @@ export const useTemplateKYCFlow = (
738
838
  }));
739
839
  return;
740
840
  }
841
+
741
842
  if (component.type === 'review_submit') {
742
- // Move to verification screen and mark verification in progress
743
843
  setState(prev => ({
744
844
  ...prev,
745
845
  currentComponentIndex: prev.currentComponentIndex + 1,
746
846
  completedComponents: [...prev.completedComponents, currentComp.id],
847
+ componentData: overrideData !== undefined ? { ...prev.componentData, [currentComp.id]: overrideData } : prev.componentData,
747
848
  isProcessing: false,
748
849
  verification: { status: 'in_progress' },
749
- errors: {
750
- ...prev.errors,
751
- [currentComp.id]: ''
752
- }
850
+ errors: { ...prev.errors, [currentComp.id]: '' }
753
851
  }));
754
852
  return;
755
853
  }
@@ -780,18 +878,16 @@ export const useTemplateKYCFlow = (
780
878
  ...prev,
781
879
  currentComponentIndex: prev.currentComponentIndex + 1,
782
880
  completedComponents: [...prev.completedComponents, currentComp.id],
881
+ componentData: overrideData !== undefined ? { ...prev.componentData, [currentComp.id]: overrideData } : prev.componentData,
783
882
  isProcessing: false,
784
- errors: {
785
- ...prev.errors,
786
- [currentComp.id]: ''
787
- }
883
+ errors: { ...prev.errors, [currentComp.id]: '' }
788
884
  }));
789
885
  return;
790
886
  }
791
887
 
792
888
  const step = serverStep === 0 && action !== 'initialize_session' ? 1 : serverStep;
793
- // Build payload data per action
794
- const payloadData = buildPayloadForComponent(action, component, state.componentData[currentComp.id], templateId, step);
889
+ const currentStepData = overrideData !== undefined ? overrideData : state.componentData[currentComp.id];
890
+ const payloadData = buildPayloadForComponent(action, component, currentStepData, templateId, step);
795
891
  console.log('payloadData', action, apiKey);
796
892
 
797
893
  await kycService.verificationSession({
@@ -804,17 +900,14 @@ export const useTemplateKYCFlow = (
804
900
  apiKey: apiKey ?? "-",
805
901
  });
806
902
  logger.log("currentComp state", truncateFields(state));
807
- // Marquer comme complété et passer au suivant
808
903
  setState(prev => ({
809
904
  ...prev,
810
905
  currentComponentIndex: prev.currentComponentIndex + 1,
811
906
  completedComponents: [...prev.completedComponents, currentComp.id],
907
+ componentData: overrideData !== undefined ? { ...prev.componentData, [currentComp.id]: overrideData } : prev.componentData,
812
908
  isProcessing: false,
813
909
  ...(action === "location_permission" ? { permissionGranted: true } : {}),
814
- errors: {
815
- ...prev.errors,
816
- [currentComp.id]: ''
817
- }
910
+ errors: { ...prev.errors, [currentComp.id]: '' }
818
911
  }));
819
912
 
820
913
  } catch (error) {
@@ -871,8 +964,8 @@ export const useTemplateKYCFlow = (
871
964
  }, [apiKey]),
872
965
 
873
966
  // Valider un composant
874
- validateComponent: useCallback((componentId: number) => {
875
- return validateComponent(componentId);
967
+ validateComponent: useCallback((componentId: number, dataOverride?: any) => {
968
+ return validateComponent(componentId, dataOverride);
876
969
  }, [validateComponent, apiKey]),
877
970
  // complet verification
878
971
  submitVerification: useCallback(async () => {
@@ -953,5 +1046,6 @@ export const useTemplateKYCFlow = (
953
1046
  getLocalizedText,
954
1047
  initializeSession,
955
1048
  env,
956
- };
1049
+ apiKey,
1050
+ } as UseTemplateReturn;
957
1051
  };
@@ -71,7 +71,8 @@ export const en = {
71
71
  privacyPolicy: 'I agree to the Privacy Policy',
72
72
  termsOfService: 'I agree to the Terms of Service',
73
73
  marketingConsent: 'I agree to receive marketing communications',
74
- readMore: 'Read more'
74
+ readMore: 'Read more',
75
+ getStarted: 'Get Started'
75
76
  },
76
77
 
77
78
  // Location Capture
@@ -126,6 +127,7 @@ export const en = {
126
127
  success: 'ID captured successfully',
127
128
  error: 'Failed to capture ID. Please try again.',
128
129
  captureTitle: '%{side} side of your government document',
130
+ cardNotFullyInFrame: 'The ID is not fully in frame. Position the entire card within the frame.',
129
131
  continueOnPhone: 'Continue on Phone',
130
132
  continueOnMobile: 'Continue on Mobile',
131
133
  scanQrCode: 'Scan this QR code with your phone to continue the verification process.',
@@ -56,7 +56,8 @@ export const fr = {
56
56
  privacyPolicy: 'J\'accepte la Politique de Confidentialité',
57
57
  termsOfService: 'J\'accepte les Conditions d\'Utilisation',
58
58
  marketingConsent: 'J\'accepte de recevoir des communications marketing',
59
- readMore: 'Lire plus'
59
+ readMore: 'Lire plus',
60
+ getStarted: 'Commencer'
60
61
  },
61
62
 
62
63
  // Location Capture
@@ -111,6 +112,7 @@ export const fr = {
111
112
  success: 'Document d\'identité capturé avec succès',
112
113
  error: 'Échec de la capture du document. Veuillez réessayer.',
113
114
  captureTitle: '%{side} de votre document d\'identité',
115
+ cardNotFullyInFrame: 'La carte n\'est pas entièrement visible. Positionnez toute la carte dans le cadre.',
114
116
  continueOnPhone: 'Continuer sur mobile',
115
117
  continueOnMobile: 'Continuer sur mobile',
116
118
  scanQrCode: 'Scannez ce code QR avec votre téléphone pour continuer la vérification.',
package/src/i18n/types.ts CHANGED
@@ -53,6 +53,8 @@ export interface KYCTranslations {
53
53
  processing: string;
54
54
  success: string;
55
55
  error: string;
56
+ captureTitle?: string;
57
+ cardNotFullyInFrame?: string;
56
58
  };
57
59
 
58
60
  selfieCapture: {
@@ -1,5 +1,5 @@
1
1
  import kycService, { authentification, errorMessage, truncateFields } from "./KYCService";
2
- import { cropByObb } from "../../utils/cropByObb";
2
+ import { cropByObb, getCardInFrame, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from "../../utils/cropByObb";
3
3
  import { GovernmentDocumentType, IBbox } from "../../types/KYC.types";
4
4
  import { KycEnvironment } from "../../types/env.types";
5
5
  import { logger } from "../../utils/logger";
@@ -40,7 +40,19 @@ export async function frontVerification(result: { path?: string, regionMapping:
40
40
  throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
41
41
  }
42
42
 
43
- // Optional: crop image using card_obb for better MRZ/barcode extraction
43
+ // If the backend indicates that the card is not fully in frame, stop early
44
+ const cardInFrame = getCardInFrame((detected as any).card_obb);
45
+ if (cardInFrame === false) {
46
+ // Use a stable error code; the UI maps this to a localized i18n message (cardNotFullyInFrame)
47
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
48
+ }
49
+
50
+ const obbConfidence = getObbConfidence((detected as any).card_obb);
51
+ if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
52
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
53
+ }
54
+
55
+ // Optional: crop image using card_obb for better MRZ/barcode extraction (only when confidence >= threshold)
44
56
  let croppedBase64: string | undefined;
45
57
  let bbox: IBbox | undefined;
46
58
  let mrz: any | undefined;
@@ -124,6 +136,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
124
136
  template_path: result?.templatePath || '',
125
137
  mrz_type: result?.mrzType || ''
126
138
  }, env);
139
+ const mrzObbConf = getObbConfidence((mrz as any).card_obb);
140
+ if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
141
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
142
+ }
127
143
  let bbox: IBbox | undefined;
128
144
  let croppedBase64: string | undefined;
129
145
 
@@ -138,6 +154,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
138
154
  logger.log("MRZ échoué, tentative d'extraction barcode");
139
155
  try {
140
156
  const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
157
+ const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
158
+ if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
159
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
160
+ }
141
161
  return barcode;
142
162
  } catch (barcodeError: any) {
143
163
  throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
@@ -164,6 +184,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
164
184
  template_path: result?.templatePath || '',
165
185
  mrz_type: result?.mrzType || ''
166
186
  }, env);
187
+ const mrzObbConf = getObbConfidence((mrz as any).card_obb);
188
+ if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
189
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
190
+ }
167
191
  let bbox: IBbox | undefined;
168
192
  try {
169
193
  const crop = await cropByObb(result?.path || '', (mrz as any).card_obb);
@@ -179,6 +203,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
179
203
  try {
180
204
  logger.log("Tentative d'extraction barcode");
181
205
  const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
206
+ const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
207
+ if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
208
+ throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
209
+ }
182
210
  let bbox: IBbox | undefined;
183
211
  try {
184
212
  const crop = await cropByObb(result?.path || '', (barcode as any).card_obb);