@sanctum-key/react-native-sdk 1.0.33 → 1.0.34
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.
- package/build/package.json +1 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +25 -18
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +82 -33
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +0 -7
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +48 -119
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +29 -22
- package/src/components/KYCElements/IDCardCapture.tsx +113 -53
- package/src/modules/api/KYCService.ts +357 -534
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions, Alert } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -96,6 +96,10 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
96
96
|
const [pendingCAImage, setPendingCAImage] = useState<{ path: string, templateResponse: any, templatePath: string, bbox?: IBbox } | null>(null);
|
|
97
97
|
|
|
98
98
|
const scannedBarcodeRef = useRef<string | null>(null);
|
|
99
|
+
// Ref mirrors — prevent stale closure reads in handleSilentCapture on iOS
|
|
100
|
+
const isAnalyzingRef = useRef<boolean>(false);
|
|
101
|
+
const isProcessingCaptureRef = useRef<boolean>(false);
|
|
102
|
+
const scanPhaseRef = useRef<'image' | 'barcode'>('image');
|
|
99
103
|
|
|
100
104
|
const [generatedCode, setGeneratedCode] = useState<string | null>(null);
|
|
101
105
|
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
|
@@ -256,6 +260,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
256
260
|
}
|
|
257
261
|
}, [value, currentSide]);
|
|
258
262
|
|
|
263
|
+
// Keep refs in sync with state so handleSilentCapture always reads current values
|
|
264
|
+
useEffect(() => { isAnalyzingRef.current = silentCaptureResult.isAnalyzing ?? false; }, [silentCaptureResult.isAnalyzing]);
|
|
265
|
+
useEffect(() => { isProcessingCaptureRef.current = isProcessingCapture; }, [isProcessingCapture]);
|
|
266
|
+
useEffect(() => { scanPhaseRef.current = scanPhase; }, [scanPhase]);
|
|
267
|
+
|
|
259
268
|
const cameraConfig = useMemo(() => {
|
|
260
269
|
let instructions = selectedDocumentType
|
|
261
270
|
? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
|
|
@@ -418,7 +427,9 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
418
427
|
const base64 = await pathToBase64(safeCroppedPath);
|
|
419
428
|
const newImages = {
|
|
420
429
|
...capturedImages,
|
|
421
|
-
|
|
430
|
+
// Store safeCroppedPath (guaranteed file:// on iOS) as dir so
|
|
431
|
+
// Image source={{ uri }} never receives a bare path on iOS.
|
|
432
|
+
[currentSide]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
422
433
|
};
|
|
423
434
|
|
|
424
435
|
setCapturedImages(newImages);
|
|
@@ -540,30 +551,44 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
540
551
|
};
|
|
541
552
|
|
|
542
553
|
const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
|
|
543
|
-
if (
|
|
544
|
-
if (
|
|
545
|
-
if (
|
|
546
|
-
|
|
554
|
+
if (isProcessingCaptureRef.current) return;
|
|
555
|
+
if (scanPhaseRef.current === 'barcode') return;
|
|
556
|
+
if (isAnalyzingRef.current) return;
|
|
557
|
+
|
|
547
558
|
if (result.success && result.path) {
|
|
548
559
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
549
|
-
|
|
560
|
+
|
|
550
561
|
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
551
562
|
let templateBbox: IBbox | undefined;
|
|
552
563
|
let templateResponse: any;
|
|
553
|
-
|
|
564
|
+
|
|
554
565
|
if (!selectedDocumentType) {
|
|
555
566
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
556
567
|
return;
|
|
557
568
|
}
|
|
558
|
-
|
|
569
|
+
|
|
559
570
|
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
571
|
+
const safeCapturePath = (Platform.OS === 'ios' && !result.path.startsWith('file://')) ? `file://${result.path}` : result.path;
|
|
572
|
+
|
|
573
|
+
// 🚨 WATCHDOG TIMER: If the iOS network drops and doesn't throw, this guarantees the UI unlocks after 20 seconds.
|
|
574
|
+
const watchdog = setTimeout(() => {
|
|
575
|
+
if (isAnalyzingRef.current) {
|
|
576
|
+
setSilentCaptureResult((prev) => ({
|
|
577
|
+
...prev,
|
|
578
|
+
isAnalyzing: false,
|
|
579
|
+
success: false,
|
|
580
|
+
error: state.currentLanguage === 'en' ? 'Connection timeout. Please try again.' : 'Délai de connexion dépassé. Veuillez réessayer.'
|
|
581
|
+
}));
|
|
582
|
+
Alert.alert(
|
|
583
|
+
state.currentLanguage === 'en' ? "Timeout" : "Délai dépassé",
|
|
584
|
+
state.currentLanguage === 'en'
|
|
585
|
+
? "The request took too long. Please verify your network and try again."
|
|
586
|
+
: "La requête a pris trop de temps. Veuillez vérifier votre connexion et réessayer."
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}, 20000);
|
|
564
590
|
|
|
565
591
|
try {
|
|
566
|
-
// 1. Check & Crop the Template visually first
|
|
567
592
|
if (!templatePath) {
|
|
568
593
|
const templateType = await checkTemplateType({
|
|
569
594
|
path: safeCapturePath,
|
|
@@ -572,63 +597,78 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
572
597
|
postfix: currentSide,
|
|
573
598
|
issuingSubdivision: issuingSubdivision
|
|
574
599
|
}, env);
|
|
575
|
-
|
|
600
|
+
|
|
576
601
|
templateResponse = templateType;
|
|
577
602
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
578
|
-
|
|
579
603
|
if (templateType.card_obb) {
|
|
580
604
|
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
581
605
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
606
|
+
clearTimeout(watchdog);
|
|
582
607
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
|
|
583
608
|
return;
|
|
584
609
|
}
|
|
585
610
|
try {
|
|
586
|
-
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
611
|
+
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
612
|
+
templateBbox = crop.bbox;
|
|
587
613
|
} catch {}
|
|
588
614
|
}
|
|
589
615
|
}
|
|
590
|
-
|
|
616
|
+
|
|
591
617
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
592
618
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
619
|
+
|
|
593
620
|
let verificationRes: any;
|
|
594
|
-
|
|
595
621
|
if (currentSide === 'front') {
|
|
596
622
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
597
623
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
598
|
-
|
|
624
|
+
|
|
599
625
|
verificationRes = await frontVerification({
|
|
600
626
|
path: safeCapturePath,
|
|
601
|
-
regionMapping: {
|
|
627
|
+
regionMapping: {
|
|
628
|
+
authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
|
|
629
|
+
mrzTypes: mrzType ? [mrzType] : []
|
|
630
|
+
},
|
|
602
631
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
603
|
-
code: countryData?.code || '',
|
|
604
|
-
|
|
632
|
+
code: countryData?.code || '',
|
|
633
|
+
currentSide,
|
|
634
|
+
templatePath,
|
|
635
|
+
mrzType,
|
|
636
|
+
issuingSubdivision: issuingSubdivision,
|
|
637
|
+
clientSideBarcodeData: silentCaptureResult.barcode
|
|
605
638
|
}, env);
|
|
639
|
+
|
|
606
640
|
} else {
|
|
607
641
|
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
608
642
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
609
643
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
610
644
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
611
|
-
|
|
612
|
-
// This validates if the back matches the front template!
|
|
645
|
+
|
|
613
646
|
verificationRes = await backVerification({
|
|
614
647
|
path: safeCapturePath,
|
|
615
|
-
regionMapping: {
|
|
648
|
+
regionMapping: {
|
|
649
|
+
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
|
|
650
|
+
mrzTypes: backMrzType ? [backMrzType] : []
|
|
651
|
+
},
|
|
616
652
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
617
|
-
code: countryData?.code || '',
|
|
618
|
-
|
|
653
|
+
code: countryData?.code || '',
|
|
654
|
+
currentSide,
|
|
655
|
+
templatePath,
|
|
656
|
+
mrzType: backMrzType,
|
|
657
|
+
templateResponse,
|
|
658
|
+
issuingSubdivision: issuingSubdivision,
|
|
659
|
+
frontTemplatePath: frontTemplatePath
|
|
619
660
|
}, env);
|
|
620
661
|
}
|
|
621
|
-
|
|
662
|
+
|
|
622
663
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
623
664
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
624
|
-
|
|
665
|
+
|
|
625
666
|
if (isBarcodeRequired()) {
|
|
626
667
|
const barcodeData = scannedBarcodeRef.current;
|
|
627
668
|
const docTypeStr = selectedDocumentType!.type;
|
|
628
669
|
const isHIC = docTypeStr === 'health_insurance_card';
|
|
629
670
|
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
630
|
-
|
|
631
|
-
// If the barcode was caught extremely fast in the same frame
|
|
671
|
+
|
|
632
672
|
if (isValidScan) {
|
|
633
673
|
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
634
674
|
let parsedDictionary;
|
|
@@ -638,14 +678,15 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
638
678
|
const token = await authentification();
|
|
639
679
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
640
680
|
}
|
|
641
|
-
|
|
681
|
+
|
|
642
682
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
643
683
|
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
644
684
|
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
645
685
|
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
646
686
|
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
647
687
|
};
|
|
648
|
-
|
|
688
|
+
|
|
689
|
+
clearTimeout(watchdog);
|
|
649
690
|
setSilentCaptureResult(verifiedResult);
|
|
650
691
|
if (bbox) {
|
|
651
692
|
const safeBbox = bbox as IBbox;
|
|
@@ -654,41 +695,60 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
654
695
|
await autoCapture(safeCapturePath!, verifiedResult);
|
|
655
696
|
return;
|
|
656
697
|
}
|
|
657
|
-
|
|
658
|
-
// If validation passed but we still need the barcode, trigger the UI phase!
|
|
698
|
+
|
|
659
699
|
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
700
|
+
clearTimeout(watchdog);
|
|
660
701
|
setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
|
|
661
702
|
setScanPhase('barcode');
|
|
662
703
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
663
704
|
return;
|
|
664
705
|
}
|
|
665
|
-
|
|
666
|
-
// 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
|
|
706
|
+
|
|
667
707
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
668
|
-
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
669
|
-
country: countryData?.code, documentType: selectedDocumentType.type,
|
|
708
|
+
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
709
|
+
isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type,
|
|
710
|
+
region: issuingSubdivision
|
|
670
711
|
};
|
|
671
712
|
|
|
713
|
+
clearTimeout(watchdog);
|
|
672
714
|
setSilentCaptureResult(verifiedResult);
|
|
673
715
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
674
716
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
675
|
-
|
|
717
|
+
|
|
676
718
|
} catch (error: any) {
|
|
719
|
+
clearTimeout(watchdog);
|
|
677
720
|
console.log("Backend Verification Error:", error);
|
|
678
|
-
const rawMessage = (error?.message || '').toLowerCase();
|
|
679
|
-
let userFriendlyMessage ='Verification failed. Please try again.';
|
|
680
721
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
722
|
+
const isNetworkError =
|
|
723
|
+
!error.response ||
|
|
724
|
+
error.message === 'Network Error' ||
|
|
725
|
+
error.code === 'ECONNABORTED' ||
|
|
726
|
+
error.message?.includes('Network');
|
|
727
|
+
|
|
728
|
+
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
729
|
+
|
|
730
|
+
if (isNetworkError) {
|
|
731
|
+
userFriendlyMessage = state.currentLanguage === 'en'
|
|
732
|
+
? 'Cannot connect to verification server. Please check your internet connection.'
|
|
733
|
+
: 'Impossible de se connecter au serveur de vérification. Veuillez vérifier votre connexion internet.';
|
|
734
|
+
|
|
735
|
+
Alert.alert(
|
|
736
|
+
state.currentLanguage === 'en' ? "Connection Failed" : "Échec de connexion",
|
|
737
|
+
userFriendlyMessage
|
|
738
|
+
);
|
|
739
|
+
} else {
|
|
740
|
+
const rawMessage = (error?.message || '').toLowerCase();
|
|
741
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
742
|
+
userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
|
|
743
|
+
} else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
744
|
+
userFriendlyMessage = 'The document does not match your selected country.';
|
|
745
|
+
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
746
|
+
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
747
|
+
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
748
|
+
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
749
|
+
}
|
|
689
750
|
}
|
|
690
|
-
|
|
691
|
-
// The error is thrown here, completely blocking the switch to 'barcode' phase
|
|
751
|
+
|
|
692
752
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
693
753
|
}
|
|
694
754
|
}
|