@sanctum-key/react-native-sdk 1.0.18 → 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 (48) hide show
  1. package/README.md +1 -1
  2. package/build/package.json +3 -2
  3. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  4. package/build/src/components/EnhancedCameraView.js +19 -182
  5. package/build/src/components/EnhancedCameraView.js.map +1 -1
  6. package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  7. package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
  8. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  9. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  10. package/build/src/components/KYCElements/IDCardCapture.js +224 -189
  11. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  12. package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/PhoneVerificationTemplate.js +0 -2
  14. package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
  15. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  16. package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
  17. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  18. package/build/src/components/OverLay/IdCard.d.ts +6 -1
  19. package/build/src/components/OverLay/IdCard.d.ts.map +1 -1
  20. package/build/src/components/OverLay/IdCard.js +36 -34
  21. package/build/src/components/OverLay/IdCard.js.map +1 -1
  22. package/build/src/config/countriesData.d.ts.map +1 -1
  23. package/build/src/config/countriesData.js.map +1 -1
  24. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  25. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  26. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  27. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  28. package/build/src/modules/api/CardAuthentification.js +67 -14
  29. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  30. package/build/src/modules/api/KYCService.d.ts +10 -0
  31. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  32. package/build/src/modules/api/KYCService.js +135 -49
  33. package/build/src/modules/api/KYCService.js.map +1 -1
  34. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  35. package/build/src/web/WebKYCEntry.js +34 -6
  36. package/build/src/web/WebKYCEntry.js.map +1 -1
  37. package/package.json +3 -2
  38. package/src/components/EnhancedCameraView.tsx +28 -219
  39. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  40. package/src/components/KYCElements/IDCardCapture.tsx +606 -570
  41. package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
  42. package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
  43. package/src/components/OverLay/IdCard.tsx +48 -36
  44. package/src/config/countriesData.ts +0 -4
  45. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  46. package/src/modules/api/CardAuthentification.ts +133 -67
  47. package/src/modules/api/KYCService.ts +189 -66
  48. 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') {
@@ -249,35 +270,78 @@ export async function backVerification(
249
270
  points = cardData[0];
250
271
  isCardInFrame = true;
251
272
  hasCroppedSides = false;
252
- // 🚨 ARTIFICIAL CHECK REMOVED HERE
253
273
  }
254
-
274
+
255
275
  if (!isCardInFrame || hasCroppedSides) {
256
276
  logger.log(`Back Framing failed. Backend reported document out of bounds.`);
257
277
  throw new Error('CARD_NOT_FULLY_IN_FRAME');
258
278
  }
259
-
279
+
260
280
  const obbConf = getObbConfidence(templateResponse?.card_obb);
261
281
  if (obbConf !== null && obbConf < OBB_CONFIDENCE_THRESHOLD) {
262
282
  throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
263
283
  }
264
-
284
+
265
285
  const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
286
+
266
287
  if (activeTemplatePath) {
267
288
  const expectedCountryName = countryData[result.code]?.name_en || '';
268
289
  const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
269
290
  const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
270
-
291
+
271
292
  if (!hasCodeMatch && !hasNameMatch) {
272
293
  logger.log(`Template mismatch! Expected: ${result.code} (${expectedCountryName}), Detected: ${activeTemplatePath}`);
273
294
  throw new Error(`Le document ne correspond pas au pays sélectionné (${result.code}).`);
274
295
  }
275
296
  }
276
297
 
277
- logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
278
-
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.");
279
325
  let extractionResult: any = {};
280
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
+
281
345
  if (hasMrz) {
282
346
  try {
283
347
  logger.log("Tentative d'extraction MRZ (Back)");
@@ -290,35 +354,31 @@ export async function backVerification(
290
354
  template_path: activeTemplatePath,
291
355
  mrz_type: result?.mrzType || 'TD1'
292
356
  }, env);
293
-
357
+
294
358
  const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
295
-
359
+
296
360
  if (mrz?.parsed_data?.status === 'FAILURE' || mrz?.success === false) {
297
361
  throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
298
362
  }
299
-
300
- extractionResult.mrz = mrz; // Attaching securely to result object
301
-
363
+ extractionResult.mrz = mrz;
302
364
  } catch (mrzError: any) {
303
365
  logger.log(`MRZ échoué: ${mrzError?.message}`);
304
- // If MRZ fails, we MUST throw an error so the user is forced to retake it!
305
366
  throw new Error(`Lecture MRZ invalide: ${mrzError?.message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
306
367
  }
307
368
  }
308
-
369
+
309
370
  if (hasBarcode) {
310
371
  try {
311
372
  logger.log("Tentative d'extraction barcode");
312
373
  const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
313
374
  extractionResult.barcode = barcode;
314
375
  } catch (e: any) {
315
- // Only throw barcode error if MRZ wasn't already successfully extracted
316
376
  if (!extractionResult.mrz) {
317
377
  throw new Error(`Lecture Barcode échouée: ${e?.message}`);
318
378
  }
319
379
  }
320
380
  }
321
-
381
+
322
382
  let bbox: IBbox | undefined;
323
383
  let croppedBase64: string | undefined;
324
384
  try {
@@ -326,7 +386,7 @@ export async function backVerification(
326
386
  bbox = crop.bbox;
327
387
  croppedBase64 = crop.base64;
328
388
  } catch { }
329
-
389
+
330
390
  return {
331
391
  success: true,
332
392
  ...extractionResult,
@@ -334,15 +394,21 @@ export async function backVerification(
334
394
  croppedBase64,
335
395
  card_obb: templateResponse.card_obb
336
396
  };
337
-
397
+
338
398
  } catch (e: any) {
339
- if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
399
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e;
340
400
  throw new Error(e?.message || 'Erreur de détection des données');
341
401
  }
342
402
  }
343
403
 
344
404
  export async function checkTemplateType(
345
- 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
+ },
346
412
  env: KycEnvironment = 'PRODUCTION'
347
413
  ) {
348
414
  try {
@@ -357,7 +423,7 @@ export async function checkTemplateType(
357
423
  }]
358
424
  };
359
425
  }
360
-
426
+
361
427
  const token = await authentification();
362
428
  const templateType = await kycService.checkTemplateType({
363
429
  fileUri: result.path || '',
@@ -366,23 +432,23 @@ export async function checkTemplateType(
366
432
  postfix: result?.postfix,
367
433
  token: token
368
434
  }, env);
369
-
435
+
370
436
  if (templateType.success === false || templateType.Error) {
371
437
  logger.log("Backend returned an error:", templateType.Error);
372
438
  throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
373
439
  }
374
-
440
+
375
441
  const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
376
442
  ? templateType.card_obb[0]
377
443
  : null;
378
-
444
+
379
445
  if (!cardData) throw new Error('CARD_NOT_FULLY_IN_FRAME');
380
-
446
+
381
447
  let points: number[][] | null = null;
448
+ console.log(points)
382
449
  let isCardInFrame = false;
383
450
  let hasCroppedSides = true;
384
- console.log(points)
385
-
451
+
386
452
  if (cardData.obb) {
387
453
  points = cardData.obb;
388
454
  if (typeof cardData.card_in_frame !== 'undefined') {
@@ -393,25 +459,25 @@ export async function checkTemplateType(
393
459
  points = cardData[0];
394
460
  isCardInFrame = true;
395
461
  hasCroppedSides = false;
396
- // 🚨 ARTIFICIAL CHECK REMOVED HERE
397
462
  }
398
-
463
+
399
464
  if (!isCardInFrame || hasCroppedSides) {
400
465
  logger.log(`Template Framing failed by backend check.`);
401
466
  throw new Error('CARD_NOT_FULLY_IN_FRAME');
402
467
  }
403
-
468
+
404
469
  const LPIPS_THRESHOLD = 0.75;
405
470
  if (templateType.lpips_score !== undefined && templateType.lpips_score > LPIPS_THRESHOLD) {
406
471
  logger.log(`🛑 Country Mismatch! LPIPS: ${templateType.lpips_score}`);
407
472
  throw new Error(`Le document présenté ne correspond pas au pays sélectionné (${result.docRegion}).`);
408
473
  }
409
-
474
+
410
475
  logger.log("templateType result", JSON.stringify(truncateFields(templateType), null, 2));
411
476
  return templateType;
477
+
412
478
  } catch (e: any) {
413
- logger.log('Errorrr checking template type:', JSON.stringify(errorMessage(e), null, 2));
414
- 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;
415
481
  throw new Error(e?.message || 'Erreur de vérification du template');
416
482
  }
417
483
  }