@transfergratis/react-native-sdk 0.1.29 → 0.1.31

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.
@@ -71,18 +71,47 @@ export async function frontVerification(
71
71
  ? detected.card_obb[0]
72
72
  : null;
73
73
 
74
- // --- STRICT FRAMING CHECK ---
75
- if (cardData && typeof cardData.card_in_frame !== 'undefined') {
76
- const isCardInFrame = cardData.card_in_frame === true;
77
- const hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
78
-
79
- // If it is NOT in the frame, OR if any side is cropped, block progression
80
- if (!isCardInFrame || hasCroppedSides) {
81
- logger.log(`Framing failed. Cropped sides: ${hasCroppedSides ? cardData.cropped_sides?.join(', ') : 'none'}`);
82
- throw new Error('CARD_NOT_FULLY_IN_FRAME');
74
+ let points = null;
75
+ let isCardInFrame = true;
76
+ let hasCroppedSides = false;
77
+
78
+ // Extract data safely
79
+ if (cardData) {
80
+ points = cardData.obb;
81
+ if (typeof cardData.card_in_frame !== 'undefined') {
82
+ isCardInFrame = cardData.card_in_frame === true;
83
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
83
84
  }
84
85
  }
85
86
 
87
+ // --- 1. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
88
+ if (!isCardInFrame || hasCroppedSides) {
89
+ logger.log(`Front Framing failed. Cropped sides detected.`);
90
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
91
+ }
92
+
93
+ // --- 2. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
94
+ if (points && points.length === 4) {
95
+ const getDistance = (p1: number[], p2: number[]) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
96
+ const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
97
+
98
+ const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
99
+ const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
100
+ const estimatedImageScale = Math.max(maxX, maxY);
101
+
102
+ const fillPercentage = longestCardEdge / estimatedImageScale;
103
+ logger.log(`Front Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
104
+
105
+ if (fillPercentage < 0.50) {
106
+ logger.log("🛑 Front Image rejected: Document is too far away.");
107
+ throw new Error("TOO_FAR_AWAY");
108
+ }
109
+ }
110
+
111
+
112
+
113
+
114
+
86
115
  // Check Confidence Threshold
87
116
  const obbConfidence = getObbConfidence(detected.card_obb);
88
117
  if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
@@ -101,7 +130,7 @@ export async function frontVerification(
101
130
 
102
131
  // MRZ Extraction
103
132
  if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ')) {
104
- mrz = await kycService.extractMrzText(
133
+ const mrzResponse = await kycService.extractMrzText(
105
134
  {
106
135
  fileUri: result.path || '',
107
136
  docType: result?.selectedDocumentType || '',
@@ -113,11 +142,20 @@ export async function frontVerification(
113
142
  },
114
143
  env
115
144
  );
145
+
146
+ // Safety check: parse if it was returned as a raw string
147
+ mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
148
+
149
+ if (mrz && (!mrz.success || mrz.parsed_data?.status === 'FAILURE')) {
150
+ logger.log("🛑 Front MRZ Extraction Failed:", mrz.parsed_data?.status_message);
151
+ throw new Error(`MRZ illisible: ${mrz.parsed_data?.status_message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
152
+ }
116
153
  }
117
154
 
118
155
  return { ...detected, croppedBase64, bbox, ...(mrz ? { mrz } : {}) };
119
156
  } catch (e: any) {
120
- logger.error('Error front verification:', JSON.stringify(errorMessage(e), null, 2));
157
+ logger.error('Error front verification:', e?.message);
158
+ // Do not use LogBox for background silent errors
121
159
  throw new Error(e?.message || 'Erreur de détection du visage');
122
160
  }
123
161
  }
@@ -250,7 +288,6 @@ export async function backVerification(
250
288
  if (!result.path) throw new Error('No path provided');
251
289
  logger.log("result.regionMapping", result.regionMapping, result.currentSide, result.code);
252
290
 
253
-
254
291
  if (env === 'SANDBOX') {
255
292
  console.log("SANDBOX mode: Skipping AI verification for back document");
256
293
 
@@ -265,27 +302,30 @@ export async function backVerification(
265
302
  if (result.regionMapping?.authMethod?.includes('MRZ')) {
266
303
  return {
267
304
  success: true,
268
- parsed_data: {
269
- status: 'success',
270
- document_type: result.selectedDocumentType,
271
- mrz_type: result.mrzType || 'TD1'
305
+ mrz: {
306
+ success: true,
307
+ parsed_data: {
308
+ status: 'SUCCESS',
309
+ document_type: result.selectedDocumentType,
310
+ mrz_type: result.mrzType || 'TD1'
311
+ }
272
312
  },
273
313
  bbox: mockBbox,
274
314
  card_obb: mockCardObb
275
315
  };
276
316
  } else if (result.regionMapping?.authMethod?.includes('2D_barcode')) {
277
317
  return {
278
- barcode_data: 'SANDBOX_MOCK_BARCODE',
318
+ success: true,
319
+ barcode: { barcode_data: 'SANDBOX_MOCK_BARCODE' },
279
320
  bbox: mockBbox,
280
321
  card_obb: mockCardObb
281
322
  };
282
323
  }
283
- return { bbox: mockBbox };
324
+ return { success: true, bbox: mockBbox, card_obb: mockCardObb };
284
325
  }
285
326
 
286
327
  const token = await authentification();
287
328
 
288
-
289
329
  logger.log("1. Checking template and framing for back document...");
290
330
 
291
331
  const templateResponse = await kycService.checkTemplateType({
@@ -296,18 +336,66 @@ export async function backVerification(
296
336
  token: token
297
337
  }, env) as ApiVerificationResponse;
298
338
 
299
- // STRICT FRAMING CHECK
339
+ if (templateResponse.success === false || templateResponse.Error) {
340
+ logger.log("Backend returned an error:", templateResponse.Error);
341
+ throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
342
+ }
343
+
300
344
  const cardData = templateResponse?.card_obb && Array.isArray(templateResponse.card_obb) && templateResponse.card_obb.length > 0
301
345
  ? templateResponse.card_obb[0]
302
346
  : null;
303
347
 
304
- if (cardData && typeof cardData.card_in_frame !== 'undefined') {
305
- const isCardInFrame = cardData.card_in_frame === true;
306
- const hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
348
+ if (!cardData) {
349
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
350
+ }
351
+
352
+ let points = null;
353
+ let isCardInFrame = false;
354
+ let hasCroppedSides = true;
307
355
 
308
- if (!isCardInFrame || hasCroppedSides) {
309
- logger.log(`Back Framing failed. Cropped sides: ${hasCroppedSides ? cardData.cropped_sides?.join(', ') : 'none'}`);
310
- throw new Error('CARD_NOT_FULLY_IN_FRAME');
356
+ // Safely extract coordinates and frame status
357
+ if (cardData.obb) {
358
+ points = cardData.obb;
359
+ if (typeof cardData.card_in_frame !== 'undefined') {
360
+ isCardInFrame = cardData.card_in_frame === true;
361
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
362
+ }
363
+ } else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
364
+ points = cardData[0];
365
+ isCardInFrame = true;
366
+ hasCroppedSides = false;
367
+
368
+ if (points && points.length === 4) {
369
+ const minX = Math.min(...points.map((p: number[]) => p[0]));
370
+ const minY = Math.min(...points.map((p: number[]) => p[1]));
371
+ if (minX <= 15 || minY <= 15) {
372
+ isCardInFrame = false;
373
+ hasCroppedSides = true;
374
+ }
375
+ }
376
+ }
377
+
378
+ // --- 2. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
379
+ if (!isCardInFrame || hasCroppedSides) {
380
+ logger.log(`Back Framing failed. Coordinates hit image boundary.`);
381
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
382
+ }
383
+
384
+ // --- 3. STRICT DISTANCE CHECK (ONLY RUNS IF FULLY IN FRAME) ---
385
+ if (points && points.length === 4) {
386
+ const getDistance = (p1: number[], p2: number[]) => Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
387
+ const longestCardEdge = Math.max(getDistance(points[0], points[1]), getDistance(points[1], points[2]));
388
+
389
+ const maxX = Math.max(points[0][0], points[1][0], points[2][0], points[3][0]);
390
+ const maxY = Math.max(points[0][1], points[1][1], points[2][1], points[3][1]);
391
+ const estimatedImageScale = Math.max(maxX, maxY);
392
+
393
+ const fillPercentage = longestCardEdge / estimatedImageScale;
394
+ logger.log(`Back Card Fill Percentage: ${(fillPercentage * 100).toFixed(1)}%`);
395
+
396
+ if (fillPercentage < 0.50) {
397
+ logger.log("🛑 Back Image rejected: Document is too far away.");
398
+ throw new Error("TOO_FAR_AWAY");
311
399
  }
312
400
  }
313
401
 
@@ -316,12 +404,10 @@ export async function backVerification(
316
404
  throw new Error('Carte non entièrement visible. Positionnez toute la carte dans le cadre.');
317
405
  }
318
406
 
319
-
320
407
  const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
321
408
 
322
409
  if (activeTemplatePath) {
323
410
  const expectedCountryName = countryData[result.code]?.name_en || '';
324
-
325
411
  const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
326
412
  const hasNameMatch = expectedCountryName && activeTemplatePath.toLowerCase().includes(expectedCountryName.toLowerCase());
327
413
 
@@ -333,88 +419,80 @@ export async function backVerification(
333
419
 
334
420
  logger.log("Framing and Country Template verified successfully. Proceeding to Data Extraction.");
335
421
 
336
-
337
- const tryMrzWithBarcodeFallback = async () => {
422
+ const hasMrz = result.regionMapping?.authMethod?.includes('MRZ') || !result.regionMapping?.authMethod?.length;
423
+ const hasBarcode = result.regionMapping?.authMethod?.includes('2D_barcode');
424
+
425
+ let extractionResult: any = {};
426
+
427
+ // --- 4. Try MRZ First (If required or default) ---
428
+ if (hasMrz) {
338
429
  try {
339
430
  logger.log("Tentative d'extraction MRZ");
340
- const mrz = await kycService.extractMrzText({
431
+ const mrzResponse = await kycService.extractMrzText({
341
432
  fileUri: result.path!,
342
433
  docType: result?.selectedDocumentType || '',
343
434
  docRegion: result?.code || '',
344
435
  postfix: 'back',
345
436
  token: token,
346
- template_path: activeTemplatePath, // Use the verified template
347
- mrz_type: result?.mrzType || ''
437
+ template_path: activeTemplatePath,
438
+ mrz_type: result?.mrzType || '' // Safe to send empty string
348
439
  }, env);
349
440
 
350
- let bbox: IBbox | undefined;
351
- let croppedBase64: string | undefined;
352
-
353
- try {
354
- // We use the OBB from our template check to ensure clean cropping
355
- const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
356
- bbox = crop.bbox;
357
- croppedBase64 = crop.base64;
358
- } catch { }
359
- return { ...mrz, bbox, croppedBase64 }
441
+ const mrz = typeof mrzResponse === 'string' ? JSON.parse(mrzResponse) : mrzResponse;
360
442
 
361
- } catch (mrzError: any) {
362
- logger.log("MRZ échoué, tentative d'extraction barcode");
363
- try {
364
- const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
365
- return barcode;
366
- } catch (barcodeError: any) {
367
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${barcodeError?.message}`);
443
+ if (!mrz || !mrz.success || mrz.parsed_data?.status === 'FAILURE') {
444
+ throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
368
445
  }
369
- }
370
- };
371
-
372
- if (result.regionMapping?.authMethod?.length > 2 && (!result?.mrzType || result?.mrzType.length === 0)) {
373
- return await tryMrzWithBarcodeFallback();
374
- }
375
-
376
- if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('MRZ') && result?.mrzType && result?.mrzType.length > 0) {
377
- try {
378
- const mrz = await kycService.extractMrzText({
379
- fileUri: result.path!,
380
- docType: result?.selectedDocumentType || '',
381
- docRegion: result?.code || '',
382
- postfix: 'back',
383
- token: token,
384
- template_path: activeTemplatePath,
385
- mrz_type: result?.mrzType || ''
386
- }, env);
387
446
 
388
- let bbox: IBbox | undefined;
389
- try {
390
- const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
391
- bbox = crop.bbox;
392
- } catch { }
393
- return { ...mrz, bbox };
447
+ extractionResult = { mrz };
448
+
394
449
  } catch (mrzError: any) {
395
- throw new Error(`MRZ et barcode ont échoué. MRZ: ${mrzError?.message}, Barcode: ${mrzError?.message}`);
450
+ if (hasBarcode) {
451
+ logger.log(`MRZ échoué (${mrzError?.message}), tentative d'extraction barcode...`);
452
+ try {
453
+ const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
454
+ extractionResult = { barcode };
455
+ } catch (barcodeError: any) {
456
+ throw new Error(`MRZ et Barcode ont échoué. Veuillez stabiliser la carte.`);
457
+ }
458
+ } else {
459
+ throw new Error(`Lecture MRZ invalide: ${mrzError?.message}`);
460
+ }
396
461
  }
397
- }
398
-
399
- if (result.regionMapping?.authMethod?.length > 0 && result.regionMapping.authMethod.includes('2D_barcode')) {
400
- try {
401
- logger.log("Tentative d'extraction barcode");
462
+ }
463
+ // --- 5. Try Barcode Only ---
464
+ else if (hasBarcode) {
465
+ try {
466
+ logger.log("Tentative d'extraction barcode seule");
402
467
  const barcode = await kycService.extractBarcode({ fileUri: result.path!, token: token }, env);
403
-
404
- let bbox: IBbox | undefined;
405
- try {
406
- const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
407
- bbox = crop.bbox;
408
- } catch { }
409
- return { ...barcode, bbox };
410
- } catch (barcodeError: any) {
411
- throw new Error(`Barcode et MRZ ont échoué. Barcode: ${barcodeError?.message}, MRZ: ${barcodeError?.message}`);
412
- }
468
+ extractionResult = { barcode };
469
+ } catch (e: any) {
470
+ throw new Error(`Lecture Barcode échouée: ${e?.message}`);
471
+ }
413
472
  }
414
- return null;
473
+
474
+ let bbox: IBbox | undefined;
475
+ let croppedBase64: string | undefined;
476
+
477
+ try {
478
+ const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
479
+ bbox = crop.bbox;
480
+ croppedBase64 = crop.base64;
481
+ } catch { }
482
+
483
+ return {
484
+ success: true,
485
+ ...extractionResult,
486
+ bbox,
487
+ croppedBase64,
488
+ card_obb: templateResponse.card_obb
489
+ };
490
+
415
491
  } catch (e: any) {
416
- if (e?.message === 'CARD_NOT_FULLY_IN_FRAME') throw e; // Bubble the strict framing error specifically
417
- throw new Error(e?.message || 'Erreur de détection du MRZ ou barcode');
492
+ if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message === 'TOO_FAR_AWAY') {
493
+ throw e;
494
+ }
495
+ throw new Error(e?.message || 'Erreur de détection des données');
418
496
  }
419
497
  }
420
498
 
@@ -441,18 +519,52 @@ export async function checkTemplateType(result: { path?: string, docType: string
441
519
  const token = await authentification();
442
520
  const templateType = await kycService.checkTemplateType({ fileUri: result.path || '', docType: result?.docType as GovernmentDocumentType, docRegion: result?.docRegion || "", postfix: result?.postfix, token: token }, env);
443
521
 
522
+ if (templateType.success === false || templateType.Error) {
523
+ logger.log("Backend returned an error:", templateType.Error);
524
+ throw new Error('Impossible de lire le document. Veuillez vous rapprocher et stabiliser la caméra.');
525
+ }
526
+
444
527
  const cardData = templateType.card_obb && Array.isArray(templateType.card_obb) && templateType.card_obb.length > 0
445
528
  ? templateType.card_obb[0]
446
529
  : null;
447
530
 
448
- if (cardData && typeof cardData.card_in_frame !== 'undefined') {
449
- const isCardInFrame = cardData.card_in_frame === true;
450
- const hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
531
+ if (!cardData) {
532
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
533
+ }
451
534
 
452
- if (!isCardInFrame || hasCroppedSides) {
453
- logger.log(`Template Framing failed. Cropped sides: ${hasCroppedSides ? cardData.cropped_sides?.join(', ') : 'none'}`);
454
- throw new Error('CARD_NOT_FULLY_IN_FRAME');
535
+ // Default to FALSE so it must prove it is in the frame
536
+ let points = null;
537
+ let isCardInFrame = false;
538
+ let hasCroppedSides = true;
539
+
540
+ // Extract data safely based on API response format
541
+ if (cardData.obb) {
542
+ points = cardData.obb;
543
+ if (typeof cardData.card_in_frame !== 'undefined') {
544
+ isCardInFrame = cardData.card_in_frame === true;
545
+ hasCroppedSides = Array.isArray(cardData.cropped_sides) && cardData.cropped_sides.length > 0;
455
546
  }
547
+ } else if (Array.isArray(cardData) && Array.isArray(cardData[0])) {
548
+ points = cardData[0];
549
+ isCardInFrame = true; // Passed initial existence check
550
+ hasCroppedSides = false;
551
+
552
+ // MATH FALLBACK: Check if the coordinates are touching the absolute edge of the photo.
553
+ if (points && points.length === 4) {
554
+ const minX = Math.min(...points.map((p: number[]) => p[0]));
555
+ const minY = Math.min(...points.map((p: number[]) => p[1]));
556
+ // If any corner is within 15 pixels of the edge, it is physically cut off!
557
+ if (minX <= 15 || minY <= 15) {
558
+ isCardInFrame = false;
559
+ hasCroppedSides = true;
560
+ }
561
+ }
562
+ }
563
+
564
+ // --- 3. STRICT FRAMING CHECK (MUST HAPPEN FIRST) ---
565
+ if (!isCardInFrame || hasCroppedSides) {
566
+ logger.log(`Template Framing failed. Coordinates hit image boundary.`);
567
+ throw new Error('CARD_NOT_FULLY_IN_FRAME');
456
568
  }
457
569
 
458
570
 
@@ -355,7 +355,18 @@ export class KYCService {
355
355
  }
356
356
 
357
357
  // STEP 3 - MRZ TEXT EXTRACTION
358
- async extractMrzText(params: { fileUri: string; docType: string; docRegion: string; postfix?: string; token: string; template_path: string; mrz_type: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
358
+ async extractMrzText(
359
+ params: {
360
+ fileUri: string;
361
+ docType: string;
362
+ docRegion: string;
363
+ postfix?: string;
364
+ token: string;
365
+ template_path: string;
366
+ mrz_type?: string;
367
+ },
368
+ env: KycEnvironment = 'PRODUCTION'
369
+ ): Promise<any> {
359
370
  // SANDBOX mode
360
371
  if (env === 'SANDBOX') {
361
372
  console.log("SANDBOX mode: Skipping AI MRZ extraction");
@@ -377,29 +388,39 @@ export class KYCService {
377
388
  const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
378
389
  const formData = new FormData();
379
390
 
380
- // ✅ FIX: Dynamic filename to prevent passports from crashing (since their MRZ is on the front)
391
+ // ✅ Dynamic filename to prevent passports from crashing (since their MRZ is on the front)
381
392
  await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
382
393
 
383
- const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
394
+ const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
384
395
  logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
385
396
 
386
- const url = `${this.mrzServiceURL}/extract_mrz_text/?doc_type=${docTypeShorted}&doc_region=${docRegion}&postfix=${postfix}&template_path=${template_path}&mrz_type=${mrz_type}`;
397
+ let url = `${this.mrzServiceURL}/extract_mrz_text/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${encodeURIComponent(postfix)}&template_path=${encodeURIComponent(template_path)}`;
398
+
399
+ if (mrz_type && mrz_type.trim() !== '') {
400
+ url += `&mrz_type=${encodeURIComponent(mrz_type)}`;
401
+ }
402
+
387
403
  logger.log("url", url);
388
404
 
389
405
  const attempt = async () => {
390
406
  try {
391
407
  const res = await axios.post<ExtractMrzTextResponse>(url, formData, {
392
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
393
- timeout: 60000,
408
+ headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
409
+ // Note: Reduced timeout to 10000ms (10s) based on our earlier network fix to prevent infinite hanging!
410
+ timeout: 10000,
394
411
  });
412
+
395
413
  logger.log('extractMrzText res', JSON.stringify(res.data, null, 2));
414
+
396
415
  if (Object.keys(res.data).length === 0) throw new Error('No data found');
397
- if (res.data?.success === false) throw new Error(res.data.parsed_data.status);
416
+ if (res.data?.success === false) throw new Error(res.data.parsed_data?.status || 'Échec de l\'extraction MRZ');
417
+
398
418
  return res.data;
399
419
  } catch (e: any) {
400
420
  throw new Error(e?.message || 'Erreur de détection du MRZ');
401
421
  }
402
422
  };
423
+
403
424
  try {
404
425
  return await attempt();
405
426
  } catch (e) {