@sanctum-key/react-native-sdk 1.0.19 → 1.0.20
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/build/package.json +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +166 -129
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts +5 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +67 -13
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +10 -0
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +94 -25
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/web/WebKYCEntry.d.ts.map +1 -1
- package/build/src/web/WebKYCEntry.js +34 -6
- package/build/src/web/WebKYCEntry.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
- package/src/components/KYCElements/IDCardCapture.tsx +613 -556
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
- package/src/hooks/useTemplateKYCFlow.tsx +14 -14
- package/src/modules/api/CardAuthentification.ts +133 -66
- package/src/modules/api/KYCService.ts +142 -38
- package/src/web/WebKYCEntry.tsx +42 -7
|
@@ -30,19 +30,20 @@ export async function frontVerification(
|
|
|
30
30
|
code: string,
|
|
31
31
|
currentSide: string,
|
|
32
32
|
templatePath?: string,
|
|
33
|
-
mrzType?: string
|
|
33
|
+
mrzType?: string,
|
|
34
|
+
issuingSubdivision?: string
|
|
34
35
|
},
|
|
35
36
|
env: KycEnvironment = 'PRODUCTION'
|
|
36
37
|
) {
|
|
37
38
|
try {
|
|
38
39
|
console.log("Front verification START", JSON.stringify({ result }, null, 2));
|
|
39
40
|
logger.log("Front verification", JSON.stringify({ result }, null, 2));
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
42
43
|
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
43
44
|
: [];
|
|
44
45
|
const hasMrz = authMethods.some(m => m.includes('MRZ'));
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
if (env === 'SANDBOX') {
|
|
47
48
|
console.log("SANDBOX mode: Skipping AI verification for front document");
|
|
48
49
|
const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
|
|
@@ -69,25 +70,25 @@ export async function frontVerification(
|
|
|
69
70
|
};
|
|
70
71
|
return mockResponse;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
const token = await authentification();
|
|
74
75
|
const detected = await kycService.detectFaceOnId(
|
|
75
76
|
result?.path || '', token, result?.selectedDocumentType || '', env
|
|
76
77
|
) as ApiVerificationResponse;
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
if (!detected.result) {
|
|
79
80
|
throw new Error('Aucun visage détecté sur la carte. Veuillez reprendre.');
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
|
|
82
83
|
const cardData = detected.card_obb && Array.isArray(detected.card_obb) && detected.card_obb.length > 0
|
|
83
84
|
? detected.card_obb[0]
|
|
84
85
|
: null;
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
let points: number[][] | null = null;
|
|
88
|
+
console.log(points)
|
|
87
89
|
let isCardInFrame = true;
|
|
88
90
|
let hasCroppedSides = false;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
if (cardData) {
|
|
92
93
|
points = cardData.obb;
|
|
93
94
|
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
@@ -95,27 +96,45 @@ export async function frontVerification(
|
|
|
95
96
|
hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
|
|
99
100
|
if (!isCardInFrame || hasCroppedSides) {
|
|
100
101
|
logger.log(`Front Framing failed. Cropped sides detected by backend.`);
|
|
101
102
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
|
|
104
105
|
const obbConfidence = getObbConfidence(detected.card_obb);
|
|
105
106
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
106
107
|
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
|
|
109
110
|
let croppedBase64: string | undefined;
|
|
110
111
|
let bbox: IBbox | undefined;
|
|
111
112
|
let mrz: any | undefined;
|
|
112
|
-
|
|
113
|
+
let documentData: any | undefined;
|
|
114
|
+
|
|
113
115
|
try {
|
|
114
116
|
const crop = await cropByObb(result?.path || '', detected.card_obb);
|
|
115
117
|
croppedBase64 = crop.base64;
|
|
116
118
|
bbox = crop.bbox;
|
|
117
119
|
} catch { }
|
|
118
120
|
|
|
121
|
+
if (result.code === 'CA') {
|
|
122
|
+
try {
|
|
123
|
+
logger.log("Tentative d'extraction OCR (Front) - Canada Only");
|
|
124
|
+
documentData = await kycService.extractDocumentInformation({
|
|
125
|
+
fileUri: result.path || '',
|
|
126
|
+
docType: result.selectedDocumentType,
|
|
127
|
+
docRegion: result.code,
|
|
128
|
+
templatePath: result.templatePath || '',
|
|
129
|
+
issuingSubdivision: result.issuingSubdivision,
|
|
130
|
+
token: token,
|
|
131
|
+
postfix: result.currentSide || 'front'
|
|
132
|
+
});
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
logger.log("Front OCR Extraction Error:", err?.message || err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
if (hasMrz) {
|
|
120
139
|
try {
|
|
121
140
|
logger.log("Tentative d'extraction MRZ (Front)");
|
|
@@ -131,11 +150,11 @@ export async function frontVerification(
|
|
|
131
150
|
},
|
|
132
151
|
env
|
|
133
152
|
);
|
|
134
|
-
|
|
153
|
+
|
|
135
154
|
mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
|
|
136
|
-
|
|
155
|
+
|
|
137
156
|
if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
|
|
138
|
-
logger.log("
|
|
157
|
+
logger.log("Front MRZ Extraction Failed:", mrz?.parsed_data?.status_message);
|
|
139
158
|
throw new Error(`MRZ illisible: ${mrz?.parsed_data?.status_message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
|
|
140
159
|
}
|
|
141
160
|
} catch (err: any) {
|
|
@@ -143,9 +162,10 @@ export async function frontVerification(
|
|
|
143
162
|
throw new Error(err?.message || "Erreur lors de l'extraction MRZ. Veuillez reprendre la photo.");
|
|
144
163
|
}
|
|
145
164
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
|
|
166
|
+
// Return detected frame, crop, MRZ, AND the new extracted OCR documentData
|
|
167
|
+
return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}), ...(documentData ? { documentData } : {}) };
|
|
168
|
+
|
|
149
169
|
} catch (e: any) {
|
|
150
170
|
logger.log('Error front verification:', e?.message);
|
|
151
171
|
throw new Error(e?.message || 'Erreur de détection du visage');
|
|
@@ -161,22 +181,23 @@ export async function backVerification(
|
|
|
161
181
|
currentSide: string,
|
|
162
182
|
templatePath?: string,
|
|
163
183
|
mrzType?: string,
|
|
164
|
-
templateResponse?: ApiVerificationResponse
|
|
184
|
+
templateResponse?: ApiVerificationResponse,
|
|
185
|
+
issuingSubdivision?: string, // Capture province/state
|
|
186
|
+
frontTemplatePath?: string // Capture the front template to compare against
|
|
165
187
|
},
|
|
166
188
|
env: KycEnvironment = 'PRODUCTION'
|
|
167
189
|
) {
|
|
168
190
|
try {
|
|
169
191
|
if (!result.path) throw new Error('No path provided');
|
|
170
192
|
logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
|
|
173
194
|
const authMethods = Array.isArray(result.regionMapping?.authMethod)
|
|
174
195
|
? result.regionMapping.authMethod.map(m => String(m).toUpperCase())
|
|
175
196
|
: [];
|
|
176
|
-
|
|
177
|
-
const hasMrz = authMethods.some(m => m.includes('MRZ'))
|
|
197
|
+
|
|
198
|
+
const hasMrz = authMethods.some(m => m.includes('MRZ'));
|
|
178
199
|
const hasBarcode = authMethods.some(m => m.includes('BARCODE') || m.includes('2D'));
|
|
179
|
-
|
|
200
|
+
|
|
180
201
|
if (env === 'SANDBOX') {
|
|
181
202
|
console.log("SANDBOX mode: Skipping AI verification for back document");
|
|
182
203
|
const mockBbox: IBbox = { minX: 400, minY: 800, width: 2200, height: 1400 };
|
|
@@ -211,34 +232,34 @@ export async function backVerification(
|
|
|
211
232
|
}
|
|
212
233
|
return { success: true, bbox: mockBbox, card_obb: mockCardObb };
|
|
213
234
|
}
|
|
214
|
-
|
|
235
|
+
|
|
215
236
|
const token = await authentification();
|
|
216
237
|
logger.log("1. Checking template and framing for back document...");
|
|
217
|
-
|
|
238
|
+
|
|
218
239
|
const templateResponse: ApiVerificationResponse = result.templateResponse ||
|
|
219
240
|
await kycService.checkTemplateType({
|
|
220
241
|
fileUri: result.path,
|
|
221
242
|
docType: result.selectedDocumentType as any,
|
|
222
243
|
docRegion: result.code,
|
|
223
244
|
postfix: 'back',
|
|
224
|
-
token: token
|
|
245
|
+
token: token,
|
|
225
246
|
}, env);
|
|
226
|
-
|
|
247
|
+
|
|
227
248
|
if (templateResponse.success === false || templateResponse.Error) {
|
|
228
249
|
throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
|
|
229
250
|
}
|
|
230
|
-
|
|
251
|
+
|
|
231
252
|
const cardData = templateResponse?.card_obb && Array.isArray(templateResponse.card_obb) && templateResponse.card_obb.length > 0
|
|
232
253
|
? templateResponse.card_obb[0]
|
|
233
254
|
: null;
|
|
234
|
-
|
|
255
|
+
|
|
235
256
|
if (!cardData) throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
236
|
-
|
|
257
|
+
|
|
237
258
|
let points: number[][] | null = null;
|
|
259
|
+
console.log(points)
|
|
238
260
|
let isCardInFrame = false;
|
|
239
261
|
let hasCroppedSides = true;
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
|
|
242
263
|
if (cardData.obb) {
|
|
243
264
|
points = cardData.obb;
|
|
244
265
|
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
@@ -250,33 +271,77 @@ export async function backVerification(
|
|
|
250
271
|
isCardInFrame = true;
|
|
251
272
|
hasCroppedSides = false;
|
|
252
273
|
}
|
|
253
|
-
|
|
274
|
+
|
|
254
275
|
if (!isCardInFrame || hasCroppedSides) {
|
|
255
276
|
logger.log(`Back Framing failed. Backend reported document out of bounds.`);
|
|
256
277
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
257
278
|
}
|
|
258
|
-
|
|
279
|
+
|
|
259
280
|
const obbConf = getObbConfidence(templateResponse?.card_obb);
|
|
260
281
|
if (obbConf !== null && obbConf < OBB_CONFIDENCE_THRESHOLD) {
|
|
261
282
|
throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
|
|
262
283
|
}
|
|
263
|
-
|
|
284
|
+
|
|
264
285
|
const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
|
|
286
|
+
|
|
265
287
|
if (activeTemplatePath) {
|
|
266
288
|
const expectedCountryName = countryData[result.code]?.name_en || '';
|
|
267
289
|
const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
|
|
268
290
|
const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
|
|
269
|
-
|
|
291
|
+
|
|
270
292
|
if (!hasCodeMatch && !hasNameMatch) {
|
|
271
293
|
logger.log(`Template mismatch! Expected: ${result.code} (${expectedCountryName}), Detected: ${activeTemplatePath}`);
|
|
272
294
|
throw new Error(`Le document ne correspond pas au pays sélectionné (${result.code}).`);
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
|
|
276
|
-
|
|
277
|
-
|
|
298
|
+
// 🚨 STRICT FRONT VS BACK TEMPLATE VALIDATION 🚨
|
|
299
|
+
if (result.frontTemplatePath && activeTemplatePath) {
|
|
300
|
+
const frontPy = result.frontTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
|
|
301
|
+
const backPy = activeTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
|
|
302
|
+
|
|
303
|
+
if (frontPy && backPy) {
|
|
304
|
+
logger.log(`Validating Front (${frontPy}) vs Back (${backPy}) for region ${result.code}`);
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
const matchValidation = await kycService.validateBackTemplate({
|
|
308
|
+
region: result.code,
|
|
309
|
+
docType: result.selectedDocumentType,
|
|
310
|
+
frontTemplateName: frontPy,
|
|
311
|
+
backTemplateName: backPy,
|
|
312
|
+
subRegion: result.issuingSubdivision || "",
|
|
313
|
+
token: token
|
|
314
|
+
}, env);
|
|
315
|
+
|
|
316
|
+
// If the API succeeds but explicitly returns false, stop immediately.
|
|
317
|
+
if (!matchValidation || matchValidation.is_match === false) {
|
|
318
|
+
logger.log(`Front/Back Mismatch: ${matchValidation?.status_message}`);
|
|
319
|
+
throw new Error(`Le verso ne correspond pas au recto fourni. Vous avez scanné un document différent.`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
logger.log("Framing, Country, and Match verified successfully. Proceeding to Data Extraction.");
|
|
278
325
|
let extractionResult: any = {};
|
|
279
326
|
|
|
327
|
+
if (result.code === 'CA') {
|
|
328
|
+
try {
|
|
329
|
+
logger.log("Tentative d'extraction OCR (Back) - Canada Only");
|
|
330
|
+
const documentData = await kycService.extractDocumentInformation({
|
|
331
|
+
fileUri: result.path!,
|
|
332
|
+
docType: result.selectedDocumentType,
|
|
333
|
+
docRegion: result.code,
|
|
334
|
+
templatePath: activeTemplatePath,
|
|
335
|
+
issuingSubdivision: result.issuingSubdivision,
|
|
336
|
+
token: token,
|
|
337
|
+
postfix: result.currentSide || 'back'
|
|
338
|
+
});
|
|
339
|
+
extractionResult.documentData = documentData;
|
|
340
|
+
} catch (err: any) {
|
|
341
|
+
logger.log("Back OCR Extraction Error:", err?.message || err);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
280
345
|
if (hasMrz) {
|
|
281
346
|
try {
|
|
282
347
|
logger.log("Tentative d'extraction MRZ (Back)");
|
|
@@ -289,35 +354,31 @@ export async function backVerification(
|
|
|
289
354
|
template_path: activeTemplatePath,
|
|
290
355
|
mrz_type: result?.mrzType || 'TD1'
|
|
291
356
|
}, env);
|
|
292
|
-
|
|
357
|
+
|
|
293
358
|
const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
|
|
294
|
-
|
|
359
|
+
|
|
295
360
|
if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
|
|
296
361
|
throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
|
|
297
362
|
}
|
|
298
|
-
|
|
299
|
-
extractionResult.mrz = mrz; // Attaching securely to result object
|
|
300
|
-
|
|
363
|
+
extractionResult.mrz = mrz;
|
|
301
364
|
} catch (mrzError: any) {
|
|
302
365
|
logger.log(`MRZ échoué: ${mrzError?.message}`);
|
|
303
|
-
// If MRZ fails, we MUST throw an error so the user is forced to retake it!
|
|
304
366
|
throw new Error(`Lecture MRZ invalide: ${mrzError?.message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
|
|
305
367
|
}
|
|
306
368
|
}
|
|
307
|
-
|
|
369
|
+
|
|
308
370
|
if (hasBarcode) {
|
|
309
371
|
try {
|
|
310
372
|
logger.log("Tentative d'extraction barcode");
|
|
311
373
|
const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
|
|
312
374
|
extractionResult.barcode = barcode;
|
|
313
375
|
} catch (e: any) {
|
|
314
|
-
// Only throw barcode error if MRZ wasn't already successfully extracted
|
|
315
376
|
if (!extractionResult.mrz) {
|
|
316
377
|
throw new Error(`Lecture Barcode échouée: ${e?.message}`);
|
|
317
378
|
}
|
|
318
379
|
}
|
|
319
380
|
}
|
|
320
|
-
|
|
381
|
+
|
|
321
382
|
let bbox: IBbox | undefined;
|
|
322
383
|
let croppedBase64: string | undefined;
|
|
323
384
|
try {
|
|
@@ -325,7 +386,7 @@ export async function backVerification(
|
|
|
325
386
|
bbox = crop.bbox;
|
|
326
387
|
croppedBase64 = crop.base64;
|
|
327
388
|
} catch { }
|
|
328
|
-
|
|
389
|
+
|
|
329
390
|
return {
|
|
330
391
|
success: true,
|
|
331
392
|
...extractionResult,
|
|
@@ -333,15 +394,21 @@ export async function backVerification(
|
|
|
333
394
|
croppedBase64,
|
|
334
395
|
card_obb: templateResponse.card_obb
|
|
335
396
|
};
|
|
336
|
-
|
|
397
|
+
|
|
337
398
|
} catch (e: any) {
|
|
338
|
-
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
|
|
399
|
+
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
|
|
339
400
|
throw new Error(e?.message || 'Erreur de détection des données');
|
|
340
401
|
}
|
|
341
402
|
}
|
|
342
403
|
|
|
343
404
|
export async function checkTemplateType(
|
|
344
|
-
result: {
|
|
405
|
+
result: {
|
|
406
|
+
path?: string,
|
|
407
|
+
docType: string,
|
|
408
|
+
docRegion: string,
|
|
409
|
+
postfix: string,
|
|
410
|
+
issuingSubdivision?: string
|
|
411
|
+
},
|
|
345
412
|
env: KycEnvironment = 'PRODUCTION'
|
|
346
413
|
) {
|
|
347
414
|
try {
|
|
@@ -356,7 +423,7 @@ export async function checkTemplateType(
|
|
|
356
423
|
}]
|
|
357
424
|
};
|
|
358
425
|
}
|
|
359
|
-
|
|
426
|
+
|
|
360
427
|
const token = await authentification();
|
|
361
428
|
const templateType = await kycService.checkTemplateType({
|
|
362
429
|
fileUri: result.path || '',
|
|
@@ -365,23 +432,23 @@ export async function checkTemplateType(
|
|
|
365
432
|
postfix: result?.postfix,
|
|
366
433
|
token: token
|
|
367
434
|
}, env);
|
|
368
|
-
|
|
435
|
+
|
|
369
436
|
if (templateType.success === false || templateType.Error) {
|
|
370
437
|
logger.log("Backend returned an error:", templateType.Error);
|
|
371
438
|
throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
|
|
372
439
|
}
|
|
373
|
-
|
|
440
|
+
|
|
374
441
|
const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
|
|
375
442
|
? templateType.card_obb[0]
|
|
376
443
|
: null;
|
|
377
|
-
|
|
444
|
+
|
|
378
445
|
if (!cardData) throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
379
|
-
|
|
446
|
+
|
|
380
447
|
let points: number[][] | null = null;
|
|
448
|
+
console.log(points)
|
|
381
449
|
let isCardInFrame = false;
|
|
382
450
|
let hasCroppedSides = true;
|
|
383
|
-
|
|
384
|
-
|
|
451
|
+
|
|
385
452
|
if (cardData.obb) {
|
|
386
453
|
points = cardData.obb;
|
|
387
454
|
if (typeof cardData.card_in_frame !== 'undefined') {
|
|
@@ -392,25 +459,25 @@ export async function checkTemplateType(
|
|
|
392
459
|
points = cardData[0];
|
|
393
460
|
isCardInFrame = true;
|
|
394
461
|
hasCroppedSides = false;
|
|
395
|
-
// 🚨 ARTIFICIAL CHECK REMOVED HERE
|
|
396
462
|
}
|
|
397
|
-
|
|
463
|
+
|
|
398
464
|
if (!isCardInFrame || hasCroppedSides) {
|
|
399
465
|
logger.log(`Template Framing failed by backend check.`);
|
|
400
466
|
throw new Error('CARD_NOT_FULLY_IN_FRAME');
|
|
401
467
|
}
|
|
402
|
-
|
|
468
|
+
|
|
403
469
|
const LPIPS_THRESHOLD = 0.75;
|
|
404
470
|
if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
|
|
405
471
|
logger.log(`🛑 Country Mismatch! LPIPS: ${templateType.lpips_score}`);
|
|
406
472
|
throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
|
|
407
473
|
}
|
|
408
|
-
|
|
474
|
+
|
|
409
475
|
logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
|
|
410
476
|
return templateType;
|
|
477
|
+
|
|
411
478
|
} catch (e: any) {
|
|
412
|
-
logger.log('
|
|
413
|
-
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) throw e;
|
|
479
|
+
logger.log('Error checking template type:', JSON.stringify(errorMessage(e), null, 2));
|
|
480
|
+
if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) throw e;
|
|
414
481
|
throw new Error(e?.message || 'Erreur de vérification du template');
|
|
415
482
|
}
|
|
416
483
|
}
|
|
@@ -281,7 +281,7 @@ export class KYCService {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
//check templatetemplate_type
|
|
284
|
-
async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string
|
|
284
|
+
async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string}, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
285
285
|
// SANDBOX mode
|
|
286
286
|
if (env === 'SANDBOX') {
|
|
287
287
|
console.log("SANDBOX mode: Skipping AI template type check");
|
|
@@ -316,29 +316,75 @@ export class KYCService {
|
|
|
316
316
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
async extractDocumentInformation(params: {
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
async extractDocumentInformation(params: {
|
|
320
|
+
fileUri: string;
|
|
321
|
+
docType: string;
|
|
322
|
+
docRegion: string;
|
|
323
|
+
templatePath: string; // 🚨 Required by API
|
|
324
|
+
issuingSubdivision?: string; // 🚨 Required for non-passports
|
|
325
|
+
token: string;
|
|
326
|
+
postfix?: string
|
|
327
|
+
}): Promise<any> {
|
|
328
|
+
const { fileUri, docType, docRegion, templatePath, issuingSubdivision, token, postfix = 'front' } = params;
|
|
322
329
|
|
|
323
|
-
//
|
|
330
|
+
// 1. Prepare Form Data
|
|
331
|
+
const formData = new FormData();
|
|
332
|
+
// Prefixing with 'front' or 'back' as strictly required by the docs
|
|
324
333
|
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
325
334
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
// 2. Build the URL with ALL required parameters
|
|
336
|
+
let queryParams = `doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}&template_path=${encodeURIComponent(templatePath)}`;
|
|
337
|
+
if (issuingSubdivision) {
|
|
338
|
+
queryParams += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
// 3. Start the Job
|
|
345
|
+
const startRes = await axios.post(url, formData, {
|
|
346
|
+
headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
|
|
330
347
|
timeout: 60000,
|
|
331
348
|
});
|
|
332
|
-
|
|
349
|
+
|
|
350
|
+
const jobId = startRes.data?.job_id;
|
|
351
|
+
if (!jobId) throw new Error("Backend did not return a job_id");
|
|
333
352
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
353
|
+
logger.log(`Extraction job started: ${jobId}. Polling for results...`);
|
|
354
|
+
|
|
355
|
+
// 4. Poll the Status Endpoint
|
|
356
|
+
const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
|
|
357
|
+
|
|
358
|
+
// Poll every 2 seconds, up to 15 times (30 seconds max)
|
|
359
|
+
const maxRetries = 15;
|
|
360
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
361
|
+
// Wait 2 seconds before checking
|
|
362
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
363
|
+
|
|
364
|
+
const statusRes = await axios.get(statusUrl, {
|
|
365
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const statusData = statusRes.data;
|
|
369
|
+
|
|
370
|
+
if (statusData.status === 'completed') {
|
|
371
|
+
logger.log('Extraction completed successfully', JSON.stringify(truncateFields(statusData.result)));
|
|
372
|
+
return statusData.result; // 👈 Returns the actual extracted data
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (statusData.status === 'failed') {
|
|
376
|
+
throw new Error(statusData.error || "Document extraction failed during processing.");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// If 'processing', loop continues...
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
throw new Error("Document extraction timed out waiting for results.");
|
|
383
|
+
|
|
384
|
+
} catch (e: any) {
|
|
385
|
+
const errorMsg = e.response?.data?.detail || e.message;
|
|
386
|
+
logger.error('Error extracting document information:', errorMsg);
|
|
387
|
+
throw new Error(errorMsg);
|
|
342
388
|
}
|
|
343
389
|
}
|
|
344
390
|
|
|
@@ -412,7 +458,7 @@ export class KYCService {
|
|
|
412
458
|
};
|
|
413
459
|
}
|
|
414
460
|
const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
|
|
415
|
-
|
|
461
|
+
|
|
416
462
|
// ... top of extractMrzText (params extraction, etc) ...
|
|
417
463
|
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
418
464
|
const safeMrzType = mrz_type && mrz_type.trim() !== '' ? mrz_type : 'TD1';
|
|
@@ -578,41 +624,53 @@ export class KYCService {
|
|
|
578
624
|
}
|
|
579
625
|
|
|
580
626
|
async verificationSession(payload: VerificationSessionRequest): Promise<any> {
|
|
581
|
-
console.log('apiKey in verificationSession', payload.apiKey);
|
|
582
627
|
try {
|
|
583
628
|
const { session_id, step, data, templateId, action, apiKey } = payload;
|
|
629
|
+
|
|
630
|
+
// 1. Safe Auth Resolution
|
|
584
631
|
const token = apiKey ? undefined : await authentification();
|
|
632
|
+
|
|
633
|
+
if (!apiKey && !token) {
|
|
634
|
+
throw new Error("Authentication failed: No API key or Token available.");
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
585
638
|
const payloadData = {
|
|
586
639
|
action: action,
|
|
587
640
|
data: {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
...
|
|
641
|
+
...data,
|
|
642
|
+
step: step,
|
|
643
|
+
templateId: templateId,
|
|
644
|
+
...(action === "location_permission" ? { permissionGranted: true } : {}),
|
|
592
645
|
},
|
|
593
|
-
|
|
594
|
-
timestamp: new Date().toISOString()
|
|
595
|
-
}
|
|
646
|
+
session_id: session_id,
|
|
647
|
+
timestamp: new Date().toISOString(),
|
|
648
|
+
};
|
|
649
|
+
|
|
596
650
|
const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`;
|
|
597
651
|
|
|
652
|
+
// 3. Clean Logging
|
|
598
653
|
const logPayload = truncateFields({ payloadData, session_id, step });
|
|
599
|
-
logger.log('verificationSession payload',
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
654
|
+
logger.log('verificationSession payload', JSON.stringify({
|
|
655
|
+
url,
|
|
656
|
+
apiKey: apiKey ? '***HIDDEN***' : 'None', // Don't log the raw API key!
|
|
657
|
+
token: token ? '***HIDDEN***' : 'None',
|
|
658
|
+
payload: logPayload
|
|
659
|
+
}, null, 2));
|
|
660
|
+
|
|
661
|
+
// 4. API Execution
|
|
662
|
+
const res = await axios.post<SessionResponse>(url, payloadData, {
|
|
606
663
|
headers: {
|
|
607
664
|
'Content-Type': 'application/json',
|
|
608
665
|
...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
|
|
609
666
|
}
|
|
610
|
-
|
|
667
|
+
});
|
|
668
|
+
|
|
611
669
|
logger.log('verificationSession res', JSON.stringify(truncateFields(res.data), null, 2));
|
|
612
670
|
return res.data;
|
|
613
671
|
|
|
614
672
|
} catch (error) {
|
|
615
|
-
logger.error('Error validating component:', JSON.stringify(error, null, 2));
|
|
673
|
+
logger.error('Error validating component:', JSON.stringify(errorMessage(error), null, 2));
|
|
616
674
|
throw new Error(errorMessage(error));
|
|
617
675
|
}
|
|
618
676
|
}
|
|
@@ -757,6 +815,54 @@ export class KYCService {
|
|
|
757
815
|
throw error;
|
|
758
816
|
}
|
|
759
817
|
}
|
|
818
|
+
|
|
819
|
+
// Inside KYCService.ts
|
|
820
|
+
|
|
821
|
+
async validateBackTemplate(params: {
|
|
822
|
+
region: string;
|
|
823
|
+
docType: string;
|
|
824
|
+
frontTemplateName: string;
|
|
825
|
+
backTemplateName: string;
|
|
826
|
+
subRegion: string;
|
|
827
|
+
token: string;
|
|
828
|
+
}, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
829
|
+
if (env === 'SANDBOX') {
|
|
830
|
+
logger.log("SANDBOX mode: Skipping back template validation");
|
|
831
|
+
return { is_match: true, status_message: "SANDBOX: Valid Match" };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const { region, docType, frontTemplateName, backTemplateName, subRegion, token } = params;
|
|
835
|
+
|
|
836
|
+
// 🚨 THE FIX: Format the payload as URL Query Parameters
|
|
837
|
+
const queryParams = new URLSearchParams({
|
|
838
|
+
region: region,
|
|
839
|
+
doc_type: docType,
|
|
840
|
+
front_template_name: frontTemplateName,
|
|
841
|
+
back_template_name: backTemplateName,
|
|
842
|
+
sub_region: subRegion || ""
|
|
843
|
+
}).toString();
|
|
844
|
+
|
|
845
|
+
const url = `${this.mrzServiceURL}/validate_back_template/?${queryParams}`;
|
|
846
|
+
|
|
847
|
+
logger.log('validateBackTemplate URL', url);
|
|
848
|
+
|
|
849
|
+
try {
|
|
850
|
+
// Pass `null` for the body since the data is now in the URL
|
|
851
|
+
const res = await axios.post(url, null, {
|
|
852
|
+
headers: {
|
|
853
|
+
'Accept': 'application/json',
|
|
854
|
+
'Authorization': `Bearer ${token}`
|
|
855
|
+
},
|
|
856
|
+
timeout: 20000,
|
|
857
|
+
});
|
|
858
|
+
logger.log('validateBackTemplate res', JSON.stringify(res.data, null, 2));
|
|
859
|
+
return res.data;
|
|
860
|
+
} catch (e: any) {
|
|
861
|
+
const errorMsg = e.response?.data?.detail || e.response?.data?.Error || e.message;
|
|
862
|
+
logger.error('Error validating back template:', JSON.stringify(errorMsg));
|
|
863
|
+
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Template validation failed');
|
|
864
|
+
}
|
|
865
|
+
}
|
|
760
866
|
}
|
|
761
867
|
|
|
762
868
|
const kycService = new KYCService("", "");
|
|
@@ -801,8 +907,6 @@ export const authentification = async (): Promise<string> => {
|
|
|
801
907
|
activeAuthRequest = (async () => {
|
|
802
908
|
logger.log('Authenticating: Fetching new Keycloak token...');
|
|
803
909
|
|
|
804
|
-
// 🚨 FIX: Do NOT use URLSearchParams in React Native with Axios.
|
|
805
|
-
// Use a raw URL-encoded string to guarantee Keycloak understands the payload.
|
|
806
910
|
const params = 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials';
|
|
807
911
|
|
|
808
912
|
const res = await axios.post(
|