@sanctum-key/react-native-sdk 1.0.19 → 1.0.21

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 (34) 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 +62 -40
  10. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  11. package/build/src/config/region_mapping.d.ts.map +1 -1
  12. package/build/src/config/region_mapping.js.map +1 -1
  13. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  14. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  15. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  16. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  17. package/build/src/modules/api/CardAuthentification.js +82 -14
  18. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  19. package/build/src/modules/api/KYCService.d.ts +11 -0
  20. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  21. package/build/src/modules/api/KYCService.js +104 -29
  22. package/build/src/modules/api/KYCService.js.map +1 -1
  23. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  24. package/build/src/web/WebKYCEntry.js +34 -6
  25. package/build/src/web/WebKYCEntry.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  28. package/src/components/KYCElements/IDCardCapture.tsx +613 -556
  29. package/src/components/KYCElements/VerificationProgressTemplate.tsx +80 -71
  30. package/src/config/region_mapping.ts +2 -1
  31. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  32. package/src/modules/api/CardAuthentification.ts +147 -67
  33. package/src/modules/api/KYCService.ts +166 -43
  34. 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,86 @@ 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
+ // 🚨 FRONT VS BACK TEMPLATE VALIDATION 🚨
299
+ if (result.frontTemplatePath && activeTemplatePath) {
300
+ try {
301
+ const frontPy = result.frontTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
302
+ const backPy = activeTemplatePath.split("/").pop()?.replace(/\.(jpg|png)$/i, ".py");
303
+
304
+ if (frontPy && backPy) {
305
+ logger.log(`Validating Front (${frontPy}) vs Back (${backPy}) for region ${result.code}`);
306
+
307
+ // 🚨 THE FIX: Only populate subRegion for Canadian 'hic' documents. Empty for all others.
308
+ const isCanadianHic = result.code === 'CA' && result.selectedDocumentType === 'hic';
309
+ const safeSubRegion = isCanadianHic ? (result.issuingSubdivision || "") : "";
310
+
311
+ const matchValidation = await kycService.validateBackTemplate({
312
+ region: result.code,
313
+ docType: result.selectedDocumentType,
314
+ frontTemplateName: frontPy,
315
+ backTemplateName: backPy,
316
+ subRegion: safeSubRegion,
317
+ token: token
318
+ }, env);
319
+
320
+ // If the API succeeds but explicitly returns false, stop immediately.
321
+ if (!matchValidation || matchValidation.is_match === false) {
322
+ logger.log(`Front/Back Mismatch: ${matchValidation?.status_message}`);
323
+ throw new Error(`Le verso ne correspond pas au recto fourni. Vous avez scanné un document différent.`);
324
+ }
325
+ }
326
+ } catch (err: any) {
327
+ // Throw hard errors (like the mismatch). Ignore harmless 400s if a template mapping doesn't exist yet
328
+ if (err?.message?.includes('ne correspond pas')) throw err;
329
+ logger.log("Template matching validation skipped/failed internally:", err?.message);
330
+ }
331
+ }
332
+
333
+ logger.log("Framing, Country, and Match verified successfully. Proceeding to Data Extraction.");
278
334
  let extractionResult: any = {};
279
335
 
336
+ if (result.code === 'CA') {
337
+ try {
338
+ logger.log("Tentative d'extraction OCR (Back) - Canada Only");
339
+ const documentData = await kycService.extractDocumentInformation({
340
+ fileUri: result.path!,
341
+ docType: result.selectedDocumentType,
342
+ docRegion: result.code,
343
+ templatePath: activeTemplatePath,
344
+ issuingSubdivision: result.issuingSubdivision,
345
+ token: token,
346
+ postfix: result.currentSide || 'back'
347
+ });
348
+ extractionResult.documentData = documentData;
349
+ } catch (err: any) {
350
+ logger.log("Back OCR Extraction Error:", err?.message || err);
351
+ }
352
+ }
353
+
280
354
  if (hasMrz) {
281
355
  try {
282
356
  logger.log("Tentative d'extraction MRZ (Back)");
@@ -289,35 +363,31 @@ export async function backVerification(
289
363
  template_path: activeTemplatePath,
290
364
  mrz_type: result?.mrzType || 'TD1'
291
365
  }, env);
292
-
366
+
293
367
  const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
294
-
368
+
295
369
  if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
296
370
  throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
297
371
  }
298
-
299
- extractionResult.mrz = mrz; // Attaching securely to result object
300
-
372
+ extractionResult.mrz = mrz;
301
373
  } catch (mrzError: any) {
302
374
  logger.log(`MRZ échoué: ${mrzError?.message}`);
303
- // If MRZ fails, we MUST throw an error so the user is forced to retake it!
304
375
  throw new Error(`Lecture MRZ invalide: ${mrzError?.message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
305
376
  }
306
377
  }
307
-
378
+
308
379
  if (hasBarcode) {
309
380
  try {
310
381
  logger.log("Tentative d'extraction barcode");
311
382
  const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
312
383
  extractionResult.barcode = barcode;
313
384
  } catch (e: any) {
314
- // Only throw barcode error if MRZ wasn't already successfully extracted
315
385
  if (!extractionResult.mrz) {
316
386
  throw new Error(`Lecture Barcode échouée: ${e?.message}`);
317
387
  }
318
388
  }
319
389
  }
320
-
390
+
321
391
  let bbox: IBbox | undefined;
322
392
  let croppedBase64: string | undefined;
323
393
  try {
@@ -325,7 +395,7 @@ export async function backVerification(
325
395
  bbox = crop.bbox;
326
396
  croppedBase64 = crop.base64;
327
397
  } catch { }
328
-
398
+
329
399
  return {
330
400
  success: true,
331
401
  ...extractionResult,
@@ -333,15 +403,21 @@ export async function backVerification(
333
403
  croppedBase64,
334
404
  card_obb: templateResponse.card_obb
335
405
  };
336
-
406
+
337
407
  } catch (e: any) {
338
- if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
408
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
339
409
  throw new Error(e?.message || 'Erreur de détection des données');
340
410
  }
341
411
  }
342
412
 
343
413
  export async function checkTemplateType(
344
- result: { path?: string, docType: string, docRegion: string, postfix: string },
414
+ result: {
415
+ path?: string,
416
+ docType: string,
417
+ docRegion: string,
418
+ postfix: string,
419
+ issuingSubdivision?: string
420
+ },
345
421
  env: KycEnvironment = 'PRODUCTION'
346
422
  ) {
347
423
  try {
@@ -356,32 +432,36 @@ export async function checkTemplateType(
356
432
  }]
357
433
  };
358
434
  }
359
-
435
+
360
436
  const token = await authentification();
437
+ const isCanada = result.docRegion?.toUpperCase() === 'CA';
438
+
361
439
  const templateType = await kycService.checkTemplateType({
362
440
  fileUri: result.path || '',
363
441
  docType: result?.docType as GovernmentDocumentType,
364
442
  docRegion: result?.docRegion || "",
365
443
  postfix: result?.postfix,
366
- token: token
444
+ token: token,
445
+ // 🚨 Pass the subdivision strictly if the region is Canada
446
+ issuingSubdivision: isCanada ? result.issuingSubdivision : undefined
367
447
  }, env);
368
-
448
+
369
449
  if (templateType.success === false || templateType.Error) {
370
450
  logger.log("Backend returned an error:", templateType.Error);
371
451
  throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
372
452
  }
373
-
453
+
374
454
  const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
375
455
  ? templateType.card_obb[0]
376
456
  : null;
377
-
457
+
378
458
  if (!cardData) throw new Error('CARD_NOT_FULLY_IN_FRAME');
379
-
459
+
380
460
  let points: number[][] | null = null;
461
+ console.log(points);
381
462
  let isCardInFrame = false;
382
463
  let hasCroppedSides = true;
383
- console.log(points)
384
-
464
+
385
465
  if (cardData.obb) {
386
466
  points = cardData.obb;
387
467
  if (typeof cardData.card_in_frame !== 'undefined') {
@@ -392,25 +472,25 @@ export async function checkTemplateType(
392
472
  points = cardData[0];
393
473
  isCardInFrame = true;
394
474
  hasCroppedSides = false;
395
- // 🚨 ARTIFICIAL CHECK REMOVED HERE
396
475
  }
397
-
476
+
398
477
  if (!isCardInFrame || hasCroppedSides) {
399
478
  logger.log(`Template Framing failed by backend check.`);
400
479
  throw new Error('CARD_NOT_FULLY_IN_FRAME');
401
480
  }
402
-
481
+
403
482
  const LPIPS_THRESHOLD = 0.75;
404
483
  if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
405
484
  logger.log(`🛑 Country Mismatch! LPIPS: ${templateType.lpips_score}`);
406
485
  throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
407
486
  }
408
-
487
+
409
488
  logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
410
489
  return templateType;
490
+
411
491
  } 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;
492
+ logger.log('Error checking template type:', JSON.stringify(errorMessage(e), null, 2));
493
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) throw e;
414
494
  throw new Error(e?.message || 'Erreur de vérification du template');
415
495
  }
416
496
  }