@transfergratis/react-native-sdk 0.1.25 → 0.1.26
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 +40 -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 +22 -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 +7 -0
- package/build/utils/cropByObb.d.ts.map +1 -1
- package/build/utils/cropByObb.js +20 -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 +41 -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 +23 -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 +20 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
let validInitialStep = 0;
|
|
154
|
+
let resumeAtIndex = 0;
|
|
147
155
|
let completedComponents: number[] = [];
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
154
|
-
const requestedComponent = templateWithReviewAndVerification.components[
|
|
155
|
-
|
|
156
|
-
logger.log('Processing
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
if (validInitialStep > 0) {
|
|
177
|
+
resumeAtIndex = requestedIndex;
|
|
178
|
+
if (resumeAtIndex > 0) {
|
|
173
179
|
completedComponents = templateWithReviewAndVerification.components
|
|
174
|
-
.slice(0,
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
validInitialStep = 0;
|
|
196
|
+
resumeAtIndex = 0;
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
} else if (requestedComponent?.type === 'review_submit') {
|
|
199
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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,
|
|
206
|
+
.slice(0, resumeAtIndex)
|
|
213
207
|
.map(component => component.id);
|
|
214
208
|
}
|
|
215
209
|
}
|
|
216
|
-
|
|
217
|
-
logger.log('Final initial state', {
|
|
218
|
-
|
|
210
|
+
|
|
211
|
+
logger.log('Final initial state (resume at component index)', {
|
|
212
|
+
resumeAtIndex,
|
|
219
213
|
completedComponentsCount: completedComponents.length,
|
|
220
|
-
|
|
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:
|
|
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
|
|
269
|
-
if (
|
|
270
|
-
logger.log('
|
|
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,
|
|
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'
|
|
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,
|
|
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
|
-
//
|
|
382
|
-
|
|
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
|
-
|
|
471
|
+
setState(prev => ({ ...prev, session: { ...prev.session, sessionDataRestored: true } }));
|
|
418
472
|
}
|
|
419
473
|
};
|
|
420
474
|
|
|
421
475
|
loadSessionData();
|
|
422
|
-
}, [existingSessionId,
|
|
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
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
? ((
|
|
622
|
+
? ((i + 1) / state.template.components.length) * 100
|
|
532
623
|
: 0;
|
|
533
|
-
}, [
|
|
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
|
-
|
|
627
|
-
|
|
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
|
|
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
|
-
|
|
794
|
-
const payloadData = buildPayloadForComponent(action, component,
|
|
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
|
};
|
package/src/i18n/en/index.ts
CHANGED
|
@@ -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.',
|
package/src/i18n/fr/index.ts
CHANGED
|
@@ -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import kycService, { authentification, errorMessage, truncateFields } from "./KYCService";
|
|
2
|
-
import { cropByObb } from "../../utils/cropByObb";
|
|
2
|
+
import { cropByObb, 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,12 @@ 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
|
-
|
|
43
|
+
const obbConfidence = getObbConfidence((detected as any).card_obb);
|
|
44
|
+
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
45
|
+
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Optional: crop image using card_obb for better MRZ/barcode extraction (only when confidence >= threshold)
|
|
44
49
|
let croppedBase64: string | undefined;
|
|
45
50
|
let bbox: IBbox | undefined;
|
|
46
51
|
let mrz: any | undefined;
|
|
@@ -124,6 +129,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
|
|
|
124
129
|
template_path: result?.templatePath || '',
|
|
125
130
|
mrz_type: result?.mrzType || ''
|
|
126
131
|
}, env);
|
|
132
|
+
const mrzObbConf = getObbConfidence((mrz as any).card_obb);
|
|
133
|
+
if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
134
|
+
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
135
|
+
}
|
|
127
136
|
let bbox: IBbox | undefined;
|
|
128
137
|
let croppedBase64: string | undefined;
|
|
129
138
|
|
|
@@ -138,6 +147,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
|
|
|
138
147
|
logger.log("MRZ échoué, tentative d'extraction barcode");
|
|
139
148
|
try {
|
|
140
149
|
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
150
|
+
const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
|
|
151
|
+
if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
152
|
+
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
153
|
+
}
|
|
141
154
|
return barcode;
|
|
142
155
|
} catch (barcodeError: any) {
|
|
143
156
|
throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
|
|
@@ -164,6 +177,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
|
|
|
164
177
|
template_path: result?.templatePath || '',
|
|
165
178
|
mrz_type: result?.mrzType || ''
|
|
166
179
|
}, env);
|
|
180
|
+
const mrzObbConf = getObbConfidence((mrz as any).card_obb);
|
|
181
|
+
if (mrzObbConf !== null && mrzObbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
182
|
+
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
183
|
+
}
|
|
167
184
|
let bbox: IBbox | undefined;
|
|
168
185
|
try {
|
|
169
186
|
const crop = await cropByObb(result?.path || '', (mrz as any).card_obb);
|
|
@@ -179,6 +196,10 @@ export async function backVerification(result: { path?: string, regionMapping: {
|
|
|
179
196
|
try {
|
|
180
197
|
logger.log("Tentative d'extraction barcode");
|
|
181
198
|
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
199
|
+
const barcodeObbConf = getObbConfidence((barcode as any).card_obb);
|
|
200
|
+
if (barcodeObbConf !== null && barcodeObbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
201
|
+
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
202
|
+
}
|
|
182
203
|
let bbox: IBbox | undefined;
|
|
183
204
|
try {
|
|
184
205
|
const crop = await cropByObb(result?.path || '', (barcode as any).card_obb);
|