@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.
Files changed (31) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  3. package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
  4. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  5. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  6. package/build/src/components/KYCElements/IDCardCapture.js +166 -129
  7. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  8. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  9. package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
  10. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  11. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  12. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  13. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  14. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  15. package/build/src/modules/api/CardAuthentification.js +67 -13
  16. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  17. package/build/src/modules/api/KYCService.d.ts +10 -0
  18. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  19. package/build/src/modules/api/KYCService.js +94 -25
  20. package/build/src/modules/api/KYCService.js.map +1 -1
  21. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  22. package/build/src/web/WebKYCEntry.js +34 -6
  23. package/build/src/web/WebKYCEntry.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  26. package/src/components/KYCElements/IDCardCapture.tsx +613 -556
  27. package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
  28. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  29. package/src/modules/api/CardAuthentification.ts +133 -66
  30. package/src/modules/api/KYCService.ts +142 -38
  31. 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
- console.log(points)
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("🛑 Front MRZ Extraction Failed:", mrz?.parsed_data?.status_message);
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
- return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
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
- logger.log('result object', result)
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
- console.log(points)
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
- logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
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: { path?: string, docType: string, docRegion: string, postfix: string },
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
- console.log(points)
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('Errorrr checking template type:', JSON.stringify(errorMessage(e), null, 2));
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 }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
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: { fileUri: string; docType: string; docRegion: string; token: string; postfix?: string }): Promise<any> {
320
- const { fileUri, docType, docRegion, token, postfix = 'front' } = params;
321
- const formData = new FormData();
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
- // ✅ FIX: Dynamic filename
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
- const url = `${this.textExtractionServiceURL}/extract_doc_information/?doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}`;
327
- const attempt = async () => {
328
- const res = await axios.post(url, formData, {
329
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
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
- logger.log('extractDocumentInformation res', JSON.stringify(truncateFields(res)));
349
+
350
+ const jobId = startRes.data?.job_id;
351
+ if (!jobId) throw new Error("Backend did not return a job_id");
333
352
 
334
- if (res.data?.detail) throw new Error(res.data.detail);
335
- return res.data;
336
- };
337
- try {
338
- return await attempt();
339
- } catch (e) {
340
- logger.error('Error extracting document information:', JSON.stringify(truncateFields(e)));
341
- throw e;
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
- step,
589
- templateId,
590
- ...(action === "location_permission" ? { permissionGranted: true, } : {}),
591
- ...data,
641
+ ...data,
642
+ step: step,
643
+ templateId: templateId,
644
+ ...(action === "location_permission" ? { permissionGranted: true } : {}),
592
645
  },
593
- ...({ session_id: session_id }),
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
- JSON.stringify(truncateFields({ logPayload, token: token ?? "-", path: url, apiKey }),
601
- null, 2))
602
-
603
- const res = await axios.post<SessionResponse>(url,
604
- payloadData,
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(