@sanctum-key/react-native-sdk 1.0.32 → 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 +2 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +35 -19
- 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 +91 -39
- 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 +2 -1
- package/src/components/EnhancedCameraView.tsx +41 -23
- package/src/components/KYCElements/IDCardCapture.tsx +121 -58
- 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,92 +551,124 @@ 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
|
-
|
|
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);
|
|
590
|
+
|
|
561
591
|
try {
|
|
562
|
-
// 1. Check & Crop the Template visually first
|
|
563
592
|
if (!templatePath) {
|
|
564
593
|
const templateType = await checkTemplateType({
|
|
565
|
-
path:
|
|
594
|
+
path: safeCapturePath,
|
|
566
595
|
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
567
596
|
docRegion: countryData?.code || "",
|
|
568
597
|
postfix: currentSide,
|
|
569
598
|
issuingSubdivision: issuingSubdivision
|
|
570
599
|
}, env);
|
|
571
|
-
|
|
600
|
+
|
|
572
601
|
templateResponse = templateType;
|
|
573
602
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
574
|
-
|
|
575
603
|
if (templateType.card_obb) {
|
|
576
604
|
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
577
605
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
606
|
+
clearTimeout(watchdog);
|
|
578
607
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
|
|
579
608
|
return;
|
|
580
609
|
}
|
|
581
610
|
try {
|
|
582
|
-
const crop = await cropByObb(
|
|
611
|
+
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
583
612
|
templateBbox = crop.bbox;
|
|
584
613
|
} catch {}
|
|
585
614
|
}
|
|
586
615
|
}
|
|
587
|
-
|
|
616
|
+
|
|
588
617
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
589
618
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
619
|
+
|
|
590
620
|
let verificationRes: any;
|
|
591
|
-
|
|
592
621
|
if (currentSide === 'front') {
|
|
593
622
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
594
623
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
595
|
-
|
|
624
|
+
|
|
596
625
|
verificationRes = await frontVerification({
|
|
597
|
-
path:
|
|
598
|
-
regionMapping: {
|
|
626
|
+
path: safeCapturePath,
|
|
627
|
+
regionMapping: {
|
|
628
|
+
authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
|
|
629
|
+
mrzTypes: mrzType ? [mrzType] : []
|
|
630
|
+
},
|
|
599
631
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
600
|
-
code: countryData?.code || '',
|
|
601
|
-
|
|
632
|
+
code: countryData?.code || '',
|
|
633
|
+
currentSide,
|
|
634
|
+
templatePath,
|
|
635
|
+
mrzType,
|
|
636
|
+
issuingSubdivision: issuingSubdivision,
|
|
637
|
+
clientSideBarcodeData: silentCaptureResult.barcode
|
|
602
638
|
}, env);
|
|
639
|
+
|
|
603
640
|
} else {
|
|
604
|
-
logger.log("Attempting backVerification with:", { path:
|
|
641
|
+
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
605
642
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
606
643
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
607
644
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
608
|
-
|
|
609
|
-
// This validates if the back matches the front template!
|
|
645
|
+
|
|
610
646
|
verificationRes = await backVerification({
|
|
611
|
-
path:
|
|
612
|
-
regionMapping: {
|
|
647
|
+
path: safeCapturePath,
|
|
648
|
+
regionMapping: {
|
|
649
|
+
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
|
|
650
|
+
mrzTypes: backMrzType ? [backMrzType] : []
|
|
651
|
+
},
|
|
613
652
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
614
|
-
code: countryData?.code || '',
|
|
615
|
-
|
|
653
|
+
code: countryData?.code || '',
|
|
654
|
+
currentSide,
|
|
655
|
+
templatePath,
|
|
656
|
+
mrzType: backMrzType,
|
|
657
|
+
templateResponse,
|
|
658
|
+
issuingSubdivision: issuingSubdivision,
|
|
659
|
+
frontTemplatePath: frontTemplatePath
|
|
616
660
|
}, env);
|
|
617
661
|
}
|
|
618
|
-
|
|
662
|
+
|
|
619
663
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
620
664
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
621
|
-
|
|
665
|
+
|
|
622
666
|
if (isBarcodeRequired()) {
|
|
623
667
|
const barcodeData = scannedBarcodeRef.current;
|
|
624
668
|
const docTypeStr = selectedDocumentType!.type;
|
|
625
669
|
const isHIC = docTypeStr === 'health_insurance_card';
|
|
626
670
|
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
627
|
-
|
|
628
|
-
// If the barcode was caught extremely fast in the same frame
|
|
671
|
+
|
|
629
672
|
if (isValidScan) {
|
|
630
673
|
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
631
674
|
let parsedDictionary;
|
|
@@ -635,57 +678,77 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
635
678
|
const token = await authentification();
|
|
636
679
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
637
680
|
}
|
|
638
|
-
|
|
681
|
+
|
|
639
682
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
640
|
-
path:
|
|
683
|
+
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
641
684
|
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
642
685
|
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
643
686
|
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
644
687
|
};
|
|
645
|
-
|
|
688
|
+
|
|
689
|
+
clearTimeout(watchdog);
|
|
646
690
|
setSilentCaptureResult(verifiedResult);
|
|
647
691
|
if (bbox) {
|
|
648
692
|
const safeBbox = bbox as IBbox;
|
|
649
693
|
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
650
694
|
}
|
|
651
|
-
await autoCapture(
|
|
695
|
+
await autoCapture(safeCapturePath!, verifiedResult);
|
|
652
696
|
return;
|
|
653
697
|
}
|
|
654
|
-
|
|
655
|
-
// If validation passed but we still need the barcode, trigger the UI phase!
|
|
698
|
+
|
|
656
699
|
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
657
|
-
|
|
700
|
+
clearTimeout(watchdog);
|
|
701
|
+
setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
|
|
658
702
|
setScanPhase('barcode');
|
|
659
703
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
660
704
|
return;
|
|
661
705
|
}
|
|
662
|
-
|
|
663
|
-
// 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
|
|
706
|
+
|
|
664
707
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
665
|
-
path:
|
|
666
|
-
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
|
|
667
711
|
};
|
|
668
712
|
|
|
713
|
+
clearTimeout(watchdog);
|
|
669
714
|
setSilentCaptureResult(verifiedResult);
|
|
670
715
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
671
|
-
await autoCapture(
|
|
672
|
-
|
|
716
|
+
await autoCapture(safeCapturePath, verifiedResult);
|
|
717
|
+
|
|
673
718
|
} catch (error: any) {
|
|
719
|
+
clearTimeout(watchdog);
|
|
674
720
|
console.log("Backend Verification Error:", error);
|
|
675
|
-
const rawMessage = (error?.message || '').toLowerCase();
|
|
676
|
-
let userFriendlyMessage ='Verification failed. Please try again.';
|
|
677
721
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
}
|
|
686
750
|
}
|
|
687
|
-
|
|
688
|
-
// The error is thrown here, completely blocking the switch to 'barcode' phase
|
|
751
|
+
|
|
689
752
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
690
753
|
}
|
|
691
754
|
}
|