@sanctum-key/react-native-sdk 1.0.50 → 1.0.52

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 (47) hide show
  1. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_file_paths.xml.flat +0 -0
  2. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -2
  3. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +1 -1
  4. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +1 -1
  5. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +1 -1
  6. package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +1 -1
  7. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -41
  8. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  9. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  10. package/android/build/outputs/logs/manifest-merger-debug-report.txt +52 -52
  11. package/build/package.json +1 -1
  12. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/IDCardCapture.js +73 -66
  14. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  15. package/build/src/components/TemplateKYCExample.d.ts +2 -1
  16. package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
  17. package/build/src/components/TemplateKYCExample.js +2 -2
  18. package/build/src/components/TemplateKYCExample.js.map +1 -1
  19. package/build/src/components/TemplateKYCFlowRefactored.d.ts +1 -0
  20. package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  21. package/build/src/components/TemplateKYCFlowRefactored.js +20 -14
  22. package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
  23. package/build/src/config/KYCConfig.d.ts.map +1 -1
  24. package/build/src/config/KYCConfig.js +4 -2
  25. package/build/src/config/KYCConfig.js.map +1 -1
  26. package/build/src/hooks/useTemplateKYCFlow.d.ts +1 -0
  27. package/build/src/hooks/useTemplateKYCFlow.d.ts.map +1 -1
  28. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  29. package/build/src/modules/api/CardAuthentification.d.ts +0 -23
  30. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  31. package/build/src/modules/api/CardAuthentification.js +46 -83
  32. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  33. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  34. package/build/src/modules/api/KYCService.js +96 -38
  35. package/build/src/modules/api/KYCService.js.map +1 -1
  36. package/build/src/modules/api/SelfieVerification.d.ts.map +1 -1
  37. package/build/src/modules/api/SelfieVerification.js +12 -19
  38. package/build/src/modules/api/SelfieVerification.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/KYCElements/IDCardCapture.tsx +192 -191
  41. package/src/components/TemplateKYCExample.tsx +3 -0
  42. package/src/components/TemplateKYCFlowRefactored.tsx +31 -25
  43. package/src/config/KYCConfig.ts +7 -5
  44. package/src/hooks/useTemplateKYCFlow.tsx +1 -0
  45. package/src/modules/api/CardAuthentification.ts +409 -445
  46. package/src/modules/api/KYCService.ts +599 -449
  47. package/src/modules/api/SelfieVerification.ts +22 -29
@@ -525,217 +525,218 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
525
525
  }
526
526
  };
527
527
 
528
- const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
529
- if (isProcessingCaptureRef.current) return;
530
- if (scanPhaseRef.current === 'barcode') return;
531
- if (isAnalyzingRef.current) return;
532
-
533
-
534
- const side = currentSideRef.current;
535
- const captured = capturedImagesRef.current;
536
- const docType = selectedDocTypeRef.current;
537
- const cData = countryDataRef.current;
528
+ const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
529
+ if (isProcessingCaptureRef.current) return;
530
+ if (scanPhaseRef.current === 'barcode') return;
531
+ if (isAnalyzingRef.current) return;
532
+
533
+ // 🚨 SYNCHRONOUS LOCK: Block all subsequent camera frames immediately
534
+ isAnalyzingRef.current = true;
535
+
536
+ const side = currentSideRef.current;
537
+ const captured = capturedImagesRef.current;
538
+ const docType = selectedDocTypeRef.current;
539
+ const cData = countryDataRef.current;
540
+
541
+ if (result.success && result.path) {
542
+ setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
543
+ const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
544
+
545
+ let templatePath = captured[side]?.templatePath || '';
546
+ let templateBbox: IBbox | undefined;
547
+ let templateResponse: any;
538
548
 
539
- if (result.success && result.path) {
540
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
541
-
542
- const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
543
- let templatePath = captured[side]?.templatePath || '';
544
- let templateBbox: IBbox | undefined;
545
- let templateResponse: any;
549
+ if (!docType) {
550
+ setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
551
+ isAnalyzingRef.current = false; // 🚨 UNLOCK
552
+ return;
553
+ }
546
554
 
547
- if (!docType) {
548
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
549
- return;
555
+ const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
556
+
557
+ const watchdog = setTimeout(() => {
558
+ if (isAnalyzingRef.current) {
559
+ isAnalyzingRef.current = false; // 🚨 UNLOCK inside watchdog
560
+ setSilentCaptureResult((prev) => ({
561
+ ...prev,
562
+ isAnalyzing: false,
563
+ success: false,
564
+ error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.'
565
+ }));
550
566
  }
567
+ }, 20000);
551
568
 
552
- const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
553
-
554
- const watchdog = setTimeout(() => {
555
- if (isAnalyzingRef.current) {
556
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.' }));
557
- }
558
- }, 20000);
559
-
560
- try {
561
- if (!templatePath) {
562
- const templateType = await checkTemplateType({ path: safeCapturePath, docType: docType.type as GovernmentDocumentType, docRegion: cData?.code || "", postfix: side, issuingSubdivision: issuingSubdivision }, env);
563
- templateResponse = templateType;
564
- if (templateType.template_path) templatePath = templateType.template_path;
565
- if (templateType.card_obb) {
566
- const obbConfidence = getObbConfidence((templateType as any).card_obb);
567
- if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
568
- clearTimeout(watchdog);
569
- setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
570
- return;
571
- }
572
- try { const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb); templateBbox = crop.bbox; } catch {}
569
+ try {
570
+ if (!templatePath) {
571
+ const templateType = await checkTemplateType({
572
+ path: safeCapturePath,
573
+ docType: docType.type as GovernmentDocumentType,
574
+ docRegion: cData?.code || "",
575
+ postfix: side,
576
+ issuingSubdivision: issuingSubdivision
577
+ }, env);
578
+
579
+ templateResponse = templateType;
580
+ if (templateType.template_path) templatePath = templateType.template_path;
581
+
582
+ if (templateType.card_obb) {
583
+ const obbConfidence = getObbConfidence((templateType as any).card_obb);
584
+ if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
585
+ clearTimeout(watchdog);
586
+ setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
587
+ isAnalyzingRef.current = false; // 🚨 UNLOCK
588
+ return;
573
589
  }
590
+ try {
591
+ const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
592
+ templateBbox = crop.bbox;
593
+ } catch {}
574
594
  }
595
+ }
575
596
 
576
- sdkLogger.info(`Template check passed for ${side}`, { templatePath });
577
- const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[cData?.code || ''] || 'root');
578
- const regionMappings = getCurrentSideVerification(side, extractedCountryKey, cData);
579
- let verificationRes: any;
580
-
581
- if (side === 'front') {
582
- const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
583
- const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
584
- verificationRes = await frontVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType, issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode }, env);
585
- } else {
586
- const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
587
- const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
588
- const frontTemplatePath = captured['front']?.templatePath || '';
589
- verificationRes = await backVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType: backMrzType, templateResponse, issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath }, env);
590
- }
597
+ sdkLogger.info(`Template check passed for ${side}`, { templatePath });
598
+
599
+ const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[cData?.code || ''] || 'root');
600
+ const regionMappings = getCurrentSideVerification(side, extractedCountryKey, cData);
601
+
602
+ let verificationRes: any;
603
+ if (side === 'front') {
604
+ const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
605
+ const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
606
+
607
+ verificationRes = await frontVerification({
608
+ path: safeCapturePath,
609
+ regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
610
+ selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
611
+ code: cData?.code || '',
612
+ currentSide: side,
613
+ templatePath,
614
+ mrzType,
615
+ issuingSubdivision: issuingSubdivision,
616
+ clientSideBarcodeData: silentCaptureResult.barcode
617
+ }, env);
618
+ } else {
619
+ const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
620
+ const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
621
+ const frontTemplatePath = captured['front']?.templatePath || '';
622
+
623
+ verificationRes = await backVerification({
624
+ path: safeCapturePath,
625
+ regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
626
+ selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
627
+ code: cData?.code || '',
628
+ currentSide: side,
629
+ templatePath,
630
+ mrzType: backMrzType,
631
+ templateResponse,
632
+ issuingSubdivision: issuingSubdivision,
633
+ frontTemplatePath: frontTemplatePath
634
+ }, env);
635
+ }
591
636
 
592
- const bbox = verificationRes?.bbox || templateBbox;
593
- const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
594
-
595
- if (isBarcodeRequired(side, cData, docType)) {
596
- const barcodeData = scannedBarcodeRef.current;
597
- const isHIC = docType.type === 'health_insurance_card';
598
- const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
599
-
600
- if (isValidScan) {
601
- let parsedDictionary;
602
- if (isHIC && barcodeData.length < 50) {
603
- parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
604
- } else {
605
- const token = await authentification();
606
- parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
607
- }
608
- const verifiedResult: ISilentCaptureResult & { region?: string } = { path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true, mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined };
609
-
610
- clearTimeout(watchdog); setSilentCaptureResult(verifiedResult);
611
- if (bbox) setBboxBySide(prev => ({ ...prev, ['back']: bbox as IBbox }));
612
- await autoCapture(safeCapturePath!, verifiedResult);
613
- return;
637
+ const bbox = verificationRes?.bbox || templateBbox;
638
+ const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
639
+
640
+ if (isBarcodeRequired(side, cData, docType)) {
641
+ const barcodeData = scannedBarcodeRef.current;
642
+ const isHIC = docType.type === 'health_insurance_card';
643
+ const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
644
+
645
+ if (isValidScan) {
646
+ let parsedDictionary;
647
+ if (isHIC && barcodeData.length < 50) {
648
+ parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
649
+ } else {
650
+ const token = await authentification();
651
+ parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
614
652
  }
615
- sdkLogger.info(`Verification passed. Waiting for barcode scan phase.`); // 🚨 LOG
653
+ const verifiedResult: ISilentCaptureResult & { region?: string } = {
654
+ path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
655
+ mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
656
+ country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined
657
+ };
658
+
616
659
  clearTimeout(watchdog);
617
- setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
618
- setScanPhase('barcode');
619
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
660
+ setSilentCaptureResult(verifiedResult);
661
+ if (bbox) setBboxBySide(prev => ({ ...prev, ['back']: bbox as IBbox }));
662
+
663
+ isAnalyzingRef.current = false; // 🚨 UNLOCK on success
664
+ await autoCapture(safeCapturePath!, verifiedResult);
620
665
  return;
621
666
  }
622
667
 
623
- const verifiedResult: ISilentCaptureResult & { region?: string } = { path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: issuingSubdivision };
668
+ sdkLogger.info(`Verification passed. Waiting for barcode scan phase.`);
624
669
  clearTimeout(watchdog);
625
- setSilentCaptureResult(verifiedResult);
626
- if (bbox) setBboxBySide(prev => ({ ...prev, [side]: bbox }));
627
- await autoCapture(safeCapturePath, verifiedResult);
670
+ setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
671
+ setScanPhase('barcode');
672
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
673
+ isAnalyzingRef.current = false; // 🚨 UNLOCK before phase shift
674
+ return;
675
+ }
628
676
 
629
- } catch (error: any) {
630
- clearTimeout(watchdog);
631
-
632
- const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
633
- const statusCode = error?.response?.status || error?.status;
634
-
635
- if (
636
- statusCode === 401 ||
637
- statusCode === 403 ||
638
- rawMessage.includes('401') ||
639
- rawMessage.includes('unauthorized') ||
640
- rawMessage.includes('token') ||
641
- rawMessage.includes('unauthenticated')
642
- ) {
643
- sdkLogger.error(`Secure API Session Expired during AI extraction`);
644
- setShowCamera(false);
645
- setIsProcessingCapture(false);
646
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: '' }));
647
-
648
- // Use React Native's native Alert to wait for the user's tap
649
- Alert.alert(
650
- state.currentLanguage === 'en' ? 'Session Expired' : 'Session Expirée',
651
- state.currentLanguage === 'en'
652
- ? 'Your secure session has expired. Please restart the process.'
653
- : 'Votre session sécurisée a expiré. Veuillez recommencer.',
654
- [
655
- {
656
- text: state.currentLanguage === 'en' ? 'Restart' : 'Recommencer',
657
- onPress: () => handleExitKYCFlow() // Redirects ONLY when they click the button
658
- }
659
- ],
660
- { cancelable: false } // Forces them to click the button
661
- );
662
-
663
- return;
664
- }
677
+ const verifiedResult: ISilentCaptureResult & { region?: string } = {
678
+ path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
679
+ country: cData?.code, documentType: docType.type, region: issuingSubdivision
680
+ };
681
+
682
+ clearTimeout(watchdog);
683
+ setSilentCaptureResult(verifiedResult);
684
+ if (bbox) setBboxBySide(prev => ({ ...prev, [side]: bbox }));
685
+
686
+ isAnalyzingRef.current = false; // 🚨 UNLOCK on final success
687
+ await autoCapture(safeCapturePath, verifiedResult);
665
688
 
666
- let hintMessage = '';
667
-
668
- // 2. Business Logic Errors
669
- if (
670
- rawMessage.includes('card_not_fully_in_frame') ||
671
- rawMessage.includes('non entièrement visible') ||
672
- rawMessage.includes('aucun visage détecté') ||
673
- rawMessage.includes('stabiliser la caméra')
674
- ) {
675
- hintMessage = state.currentLanguage === 'en'
676
- ? 'Ensure the entire card and your face are clearly inside the frame.'
677
- : 'Assurez-vous que toute la carte et le visage sont clairement dans le cadre.';
678
- }
679
-
680
- // 2. Document/Country Mismatch
681
- // Catches: 'pays sélectionné', 'LPIPS'
682
- else if (rawMessage.includes('pays sélectionné')) {
683
- hintMessage = state.currentLanguage === 'en'
684
- ? 'This document does not match the selected country.'
685
- : 'Ce document ne correspond pas au pays sélectionné.';
686
- }
687
-
688
- // 3. Front/Back Document Mismatch
689
- // Catches: 'ne correspond pas au recto', 'document différent'
690
- else if (rawMessage.includes('ne correspond pas au recto') || rawMessage.includes('document différent')) {
691
- hintMessage = state.currentLanguage === 'en'
692
- ? 'Please scan the back of the exact same ID.'
693
- : 'Veuillez scanner le verso de la même pièce d\'identité.';
694
- }
695
-
696
- // 4. Data Extraction Failures (OCR & MRZ)
697
- // Catches: 'impossible de lire', 'impossible d'extraire', 'mrz illisible', 'lecture mrz invalide'
698
- else if (
699
- rawMessage.includes('impossible de lire') ||
700
- rawMessage.includes('impossible d\'extraire') ||
701
- rawMessage.includes('mrz') ||
702
- rawMessage.includes('extraction') ||
703
- rawMessage.includes('illisible') ||
704
- rawMessage.includes('invalide')
705
- ) {
706
- hintMessage = state.currentLanguage === 'en'
707
- ? 'Cannot read document text. Avoid glare, improve lighting, and hold steady.'
708
- : 'Impossible de lire le texte. Évitez les reflets, améliorez l\'éclairage et restez immobile.';
709
- }
710
-
711
- // 5. Server Infrastructure & Downtime
712
- // Catches: Cloudflare tunnel errors, 502/503 Gateway timeouts
713
- else if (
714
- rawMessage.includes('cloudflare') ||
715
- rawMessage.includes('tunnel') ||
716
- rawMessage.includes('502') ||
717
- rawMessage.includes('503')
718
- ) {
719
- hintMessage = state.currentLanguage === 'en'
720
- ? 'Server is temporarily offline. Please try again in a few minutes.'
721
- : 'Le serveur est temporairement hors ligne. Veuillez réessayer dans quelques minutes.';
722
- }
689
+ } catch (error: any) {
690
+ clearTimeout(watchdog);
691
+ const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
692
+ const statusCode = error?.response?.status || error?.status;
693
+
694
+ if (
695
+ statusCode === 401 || statusCode === 403 ||
696
+ rawMessage.includes('401') || rawMessage.includes('unauthorized') ||
697
+ rawMessage.includes('token') || rawMessage.includes('unauthenticated')
698
+ ) {
699
+ sdkLogger.error(`Secure API Session Expired during AI extraction`);
700
+ setShowCamera(false);
701
+ setIsProcessingCapture(false);
702
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: '' }));
723
703
 
724
- // 6. True Network Errors vs. Fallback Processing
725
- else {
726
- // Strictly detect Axios network drops without swallowing unknown custom errors
727
- const isNetworkError = error?.isAxiosError || rawMessage === 'network error' || error?.code === 'econnaborted' || rawMessage.includes('network');
728
-
729
- if (isNetworkError) {
730
- hintMessage = state.currentLanguage === 'en'
731
- ? 'Poor connection. Please check your internet.'
732
- : 'Connexion faible. Veuillez vérifier votre internet.';
733
- }
704
+ Alert.alert(
705
+ state.currentLanguage === 'en' ? 'Session Expired' : 'Session Expirée',
706
+ state.currentLanguage === 'en' ? 'Your secure session has expired. Please restart the process.' : 'Votre session sécurisée a expiré. Veuillez recommencer.',
707
+ [{ text: state.currentLanguage === 'en' ? 'Restart' : 'Recommencer', onPress: () => handleExitKYCFlow() }],
708
+ { cancelable: false }
709
+ );
710
+ isAnalyzingRef.current = false; // 🚨 UNLOCK on session expiry
711
+ return;
712
+ }
713
+
714
+ let hintMessage = '';
715
+ if (rawMessage.includes('card_not_fully_in_frame') || rawMessage.includes('non entièrement visible') || rawMessage.includes('aucun visage détecté') || rawMessage.includes('stabiliser la caméra')) {
716
+ hintMessage = state.currentLanguage === 'en' ? 'Ensure the entire card and your face are clearly inside the frame.' : 'Assurez-vous que toute la carte et le visage sont clairement dans le cadre.';
717
+ } else if (rawMessage.includes('pays sélectionné')) {
718
+ hintMessage = state.currentLanguage === 'en' ? 'This document does not match the selected country.' : 'Ce document ne correspond pas au pays sélectionné.';
719
+ } else if (rawMessage.includes('ne correspond pas au recto') || rawMessage.includes('document différent')) {
720
+ hintMessage = state.currentLanguage === 'en' ? 'Please scan the back of the exact same ID.' : 'Veuillez scanner le verso de la même pièce d\'identité.';
721
+ } else if (rawMessage.includes('impossible de lire') || rawMessage.includes('impossible d\'extraire') || rawMessage.includes('mrz') || rawMessage.includes('extraction') || rawMessage.includes('illisible') || rawMessage.includes('invalide')) {
722
+ hintMessage = state.currentLanguage === 'en' ? 'Cannot read document text. Avoid glare, improve lighting, and hold steady.' : 'Impossible de lire le texte. Évitez les reflets, améliorez l\'éclairage et restez immobile.';
723
+ } else if (rawMessage.includes('cloudflare') || rawMessage.includes('tunnel') || rawMessage.includes('502') || rawMessage.includes('503')) {
724
+ hintMessage = state.currentLanguage === 'en' ? 'Server is temporarily offline. Please try again in a few minutes.' : 'Le serveur est temporairement hors ligne. Veuillez réessayer dans quelques minutes.';
725
+ } else {
726
+ const isNetworkError = error?.isAxiosError || rawMessage === 'network error' || error?.code === 'econnaborted' || rawMessage.includes('network');
727
+ if (isNetworkError) {
728
+ hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Please check your internet.' : 'Connexion faible. Veuillez vérifier votre internet.';
734
729
  }
735
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: hintMessage }));
736
730
  }
731
+
732
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: hintMessage }));
733
+ isAnalyzingRef.current = false; // 🚨 UNLOCK at end of catch block
737
734
  }
738
- };
735
+ } else {
736
+ // 🚨 UNLOCK if the camera frame was invalid or empty
737
+ isAnalyzingRef.current = false;
738
+ }
739
+ };
739
740
 
740
741
  useEffect(() => {
741
742
  if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
@@ -48,6 +48,7 @@ export interface TemplateKYCExampleProps {
48
48
  onError: (error: string) => void;
49
49
  language: string;
50
50
  API_KEY?: string;
51
+ TEST_API_KEY?: string;
51
52
  templateId?: string;
52
53
  template?: KYCTemplate;
53
54
  env?: KycEnvironment;
@@ -66,6 +67,7 @@ export function TemplateKYCExample({
66
67
  onError,
67
68
  language,
68
69
  API_KEY,
70
+ TEST_API_KEY,
69
71
  templateId,
70
72
  template,
71
73
  env = 'PRODUCTION',
@@ -155,6 +157,7 @@ export function TemplateKYCExample({
155
157
  onCancel={handleCancel}
156
158
  language={language}
157
159
  API_KEY={API_KEY}
160
+ TEST_API_KEY={TEST_API_KEY}
158
161
  env={env}
159
162
  serverEnv={serverEnv}
160
163
  existingSessionId={existingSessionId}
@@ -4,7 +4,6 @@ import { KYCTemplate, VerificationState } from '../types/KYC.types';
4
4
  import { TemplateKYCFlowProvider, useTemplateKYCFlowContext } from '../hooks/useTemplateKYCFlow';
5
5
  import { useI18n } from '../hooks/useI18n';
6
6
  import { showAlert } from '../utils/platformAlert';
7
-
8
7
  import { IDCardCapture } from './KYCElements/IDCardCapture';
9
8
  import { SelfieCaptureTemplate } from './KYCElements/SelfieCaptureTemplate';
10
9
  import { FileUploadTemplate } from './KYCElements/FileUploadTemplate';
@@ -21,18 +20,19 @@ import { PersonalInformationTemplate } from './KYCElements/PersonalInformationTe
21
20
  import { AdditionalDocumentsTemplate } from './KYCElements/AdditionalDocumentsTemplate';
22
21
  import { KycEnvironment, BackendEnvironment } from '../types/env.types';
23
22
  import KYCConfig from '../config/KYCConfig';
24
- import { sdkLogger } from '../utils/logger'; // 🚨 IMPORT SDK LOGGER
23
+ import { logger, sdkLogger } from '../utils/logger';
25
24
 
26
25
  interface TemplateKYCFlowProps {
27
- template?: KYCTemplate; // Format SDK direct (existing, now optional)
28
- templateId?: string; // New: ID to load from API
26
+ template?: KYCTemplate;
27
+ templateId?: string;
29
28
  onComplete?: (data: VerificationState) => void;
30
29
  onError?: (error: string) => void;
31
30
  language?: string;
32
31
  onCancel?: () => void;
33
- API_KEY?: string; // Required if templateId is used
34
- env?: KycEnvironment; // Flow execution: PRODUCTION (full AI) or SANDBOX (skip AI)
35
- serverEnv?: BackendEnvironment; // Backend to call: PRODUCTION or TEST (API URL)
32
+ API_KEY?: string;
33
+ TEST_API_KEY?: string;
34
+ env?: KycEnvironment;
35
+ serverEnv?: BackendEnvironment;
36
36
  existingSessionId?: string;
37
37
  initialComponentIndex?: number;
38
38
  initialCountryResume?: { code: string; documentType: string; region?: string };
@@ -49,6 +49,7 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
49
49
  language = 'fr',
50
50
  onCancel,
51
51
  API_KEY,
52
+ TEST_API_KEY,
52
53
  env = 'PRODUCTION',
53
54
  serverEnv,
54
55
  existingSessionId,
@@ -61,12 +62,14 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
61
62
  const { t } = useI18n();
62
63
  const { template: loadedTemplate, isLoading, error, loadTemplate } = useTemplateLoader();
63
64
 
65
+ // 🚨 1. Determine the active API key based on the environment
66
+ const activeApiKey = env === 'PRODUCTION' ? API_KEY : (TEST_API_KEY || API_KEY);
67
+
64
68
  useEffect(() => {
65
- if (API_KEY) {
66
- if (sdkLogger.setApiKey) sdkLogger.setApiKey(API_KEY);
69
+ if (activeApiKey) {
70
+ if (sdkLogger.setApiKey) sdkLogger.setApiKey(activeApiKey);
67
71
  }
68
-
69
- }, [env, serverEnv, existingSessionId, API_KEY]);
72
+ }, [env, serverEnv, existingSessionId, activeApiKey]);
70
73
 
71
74
  useEffect(() => {
72
75
  if (serverEnv !== undefined) {
@@ -77,19 +80,20 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
77
80
  useEffect(() => {
78
81
  if (!providedTemplate && !templateId) {
79
82
  const errorMsg = 'Either template or templateId must be provided';
80
- sdkLogger.error("Template Loading Failed: Missing Props", { errorMsg });
83
+ logger.error("Template Loading Failed: Missing Props", { errorMsg });
81
84
  onError?.(errorMsg);
82
85
  return;
83
86
  }
84
87
 
85
- if (templateId && !API_KEY) {
86
- sdkLogger.info(`Fetching template ${templateId} via token authentication`);
88
+ if (templateId && !activeApiKey) {
89
+ logger.log(`Fetching template ${templateId} via token authentication`);
87
90
  loadTemplate(templateId);
88
- } else if (templateId && API_KEY) {
89
- sdkLogger.info(`Fetching template ${templateId} via API Key`);
90
- loadTemplate(templateId, API_KEY);
91
+ } else if (templateId && activeApiKey) {
92
+ logger.log(`Fetching template ${templateId} via API Key`);
93
+ // 🚨 2. Use the resolved active key here
94
+ loadTemplate(templateId, activeApiKey);
91
95
  }
92
- }, [templateId, API_KEY, providedTemplate, loadTemplate, onError]);
96
+ }, [templateId, activeApiKey, providedTemplate, loadTemplate, onError]);
93
97
 
94
98
  useEffect(() => {
95
99
  if (error) {
@@ -103,8 +107,8 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
103
107
  t('kyc.cancelConfirmation.title'),
104
108
  t('kyc.cancelConfirmation.message'),
105
109
  [
106
- {
107
- text: t('kyc.cancelConfirmation.cancelButton'),
110
+ {
111
+ text: t('kyc.cancelConfirmation.cancelButton'),
108
112
  style: 'cancel',
109
113
  },
110
114
  {
@@ -140,11 +144,11 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
140
144
  return (
141
145
  <View style={styles.errorContainer}>
142
146
  <Text style={styles.errorText}>{error}</Text>
143
- <TouchableOpacity
144
- style={styles.retryButton}
147
+ <TouchableOpacity
148
+ style={styles.retryButton}
145
149
  onPress={() => {
146
150
  sdkLogger.info("User retrying template fetch");
147
- loadTemplate(templateId, API_KEY);
151
+ loadTemplate(templateId, activeApiKey);
148
152
  }}
149
153
  >
150
154
  <Text style={styles.retryButtonText}>{t('common.retry') || 'Retry'}</Text>
@@ -163,7 +167,6 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
163
167
  );
164
168
  }
165
169
 
166
-
167
170
  return (
168
171
  <TemplateKYCFlowProvider
169
172
  template={template}
@@ -179,7 +182,10 @@ export const TemplateKYCFlow: React.FC<TemplateKYCFlowProps> = ({
179
182
  }}
180
183
  onCancel={OnCancel}
181
184
  initialLanguage={language}
182
- apiKey={API_KEY}
185
+ // 🚨 3. Pass the resolved active API key down to the provider!
186
+ // The context/hooks will automatically use this key.
187
+ apiKey={activeApiKey}
188
+ testApiKey={TEST_API_KEY}
183
189
  env={env}
184
190
  existingSessionId={existingSessionId}
185
191
  initialComponentIndex={initialComponentIndex}
@@ -4,10 +4,11 @@ class KYCConfig {
4
4
  private static instance: KYCConfig;
5
5
  private backendEnvironment: BackendEnvironment = 'PRODUCTION';
6
6
 
7
- private backendUrls: Record<BackendEnvironment, string> = {
7
+ // 🚨 Use a string record to safely handle 'TEST' or 'SANDBOX' aliases
8
+ private backendUrls: Record<string, string> = {
8
9
  PRODUCTION: 'https://service.sanctumkey.com/api/v1',
9
- // TEST: 'https://kyc-backend.transfergratis.net/api/v1',
10
- TEST: 'https://api.sanctumkey.com/api/v1',
10
+ TEST: 'https://api.sanctumkey.com/api/v1',
11
+ SANDBOX: 'https://kyc-backend.transfergratis.net/api/v1',
11
12
  };
12
13
 
13
14
  private constructor() { }
@@ -24,7 +25,8 @@ class KYCConfig {
24
25
  }
25
26
 
26
27
  public getBackendUrl(): string {
27
- return this.backendUrls[this.backendEnvironment];
28
+ // Fallback to PRODUCTION if an invalid env string is somehow passed
29
+ return this.backendUrls[this.backendEnvironment] || this.backendUrls['PRODUCTION'];
28
30
  }
29
31
 
30
32
  public getBackendEnvironment(): BackendEnvironment {
@@ -32,4 +34,4 @@ class KYCConfig {
32
34
  }
33
35
  }
34
36
 
35
- export default KYCConfig.getInstance();
37
+ export default KYCConfig.getInstance();
@@ -33,6 +33,7 @@ interface TemplateKYCFlowProviderProps {
33
33
  onCancel?: () => void;
34
34
  initialLanguage?: string;
35
35
  apiKey?: string;
36
+ testApiKey?: string;
36
37
  env?: KycEnvironment;
37
38
  existingSessionId?: string;
38
39
  /** Index in template.components where to resume (0-based). Simple to store in template table. */