@sanctum-key/react-native-sdk 1.0.33 → 1.0.35
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 +82 -28
- 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 +84 -33
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +10 -18
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +183 -588
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +92 -33
- package/src/components/KYCElements/IDCardCapture.tsx +116 -53
- package/src/modules/api/KYCService.ts +255 -938
|
@@ -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,47 @@ 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
|
+
|
|
561
|
+
const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://')
|
|
562
|
+
? `file://${result.path}`
|
|
563
|
+
: result.path;
|
|
564
|
+
|
|
550
565
|
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
551
566
|
let templateBbox: IBbox | undefined;
|
|
552
567
|
let templateResponse: any;
|
|
553
|
-
|
|
568
|
+
|
|
554
569
|
if (!selectedDocumentType) {
|
|
555
570
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
556
571
|
return;
|
|
557
572
|
}
|
|
558
|
-
|
|
573
|
+
|
|
559
574
|
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
|
|
576
|
+
// 🚨 WATCHDOG TIMER: If the iOS network drops and doesn't throw, this guarantees the UI unlocks after 20 seconds.
|
|
577
|
+
const watchdog = setTimeout(() => {
|
|
578
|
+
if (isAnalyzingRef.current) {
|
|
579
|
+
setSilentCaptureResult((prev) => ({
|
|
580
|
+
...prev,
|
|
581
|
+
isAnalyzing: false,
|
|
582
|
+
success: false,
|
|
583
|
+
error: state.currentLanguage === 'en' ? 'Connection timeout. Please try again.' : 'Délai de connexion dépassé. Veuillez réessayer.'
|
|
584
|
+
}));
|
|
585
|
+
Alert.alert(
|
|
586
|
+
state.currentLanguage === 'en' ? "Timeout" : "Délai dépassé",
|
|
587
|
+
state.currentLanguage === 'en'
|
|
588
|
+
? "The request took too long. Please verify your network and try again."
|
|
589
|
+
: "La requête a pris trop de temps. Veuillez vérifier votre connexion et réessayer."
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}, 20000);
|
|
564
593
|
|
|
565
594
|
try {
|
|
566
|
-
// 1. Check & Crop the Template visually first
|
|
567
595
|
if (!templatePath) {
|
|
568
596
|
const templateType = await checkTemplateType({
|
|
569
597
|
path: safeCapturePath,
|
|
@@ -572,63 +600,78 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
572
600
|
postfix: currentSide,
|
|
573
601
|
issuingSubdivision: issuingSubdivision
|
|
574
602
|
}, env);
|
|
575
|
-
|
|
603
|
+
|
|
576
604
|
templateResponse = templateType;
|
|
577
605
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
578
|
-
|
|
579
606
|
if (templateType.card_obb) {
|
|
580
607
|
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
581
608
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
609
|
+
clearTimeout(watchdog);
|
|
582
610
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
|
|
583
611
|
return;
|
|
584
612
|
}
|
|
585
613
|
try {
|
|
586
|
-
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
614
|
+
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
615
|
+
templateBbox = crop.bbox;
|
|
587
616
|
} catch {}
|
|
588
617
|
}
|
|
589
618
|
}
|
|
590
|
-
|
|
619
|
+
|
|
591
620
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
592
621
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
622
|
+
|
|
593
623
|
let verificationRes: any;
|
|
594
|
-
|
|
595
624
|
if (currentSide === 'front') {
|
|
596
625
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
597
626
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
598
|
-
|
|
627
|
+
|
|
599
628
|
verificationRes = await frontVerification({
|
|
600
629
|
path: safeCapturePath,
|
|
601
|
-
regionMapping: {
|
|
630
|
+
regionMapping: {
|
|
631
|
+
authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
|
|
632
|
+
mrzTypes: mrzType ? [mrzType] : []
|
|
633
|
+
},
|
|
602
634
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
603
|
-
code: countryData?.code || '',
|
|
604
|
-
|
|
635
|
+
code: countryData?.code || '',
|
|
636
|
+
currentSide,
|
|
637
|
+
templatePath,
|
|
638
|
+
mrzType,
|
|
639
|
+
issuingSubdivision: issuingSubdivision,
|
|
640
|
+
clientSideBarcodeData: silentCaptureResult.barcode
|
|
605
641
|
}, env);
|
|
642
|
+
|
|
606
643
|
} else {
|
|
607
644
|
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
608
645
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
609
646
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
610
647
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
611
|
-
|
|
612
|
-
// This validates if the back matches the front template!
|
|
648
|
+
|
|
613
649
|
verificationRes = await backVerification({
|
|
614
650
|
path: safeCapturePath,
|
|
615
|
-
regionMapping: {
|
|
651
|
+
regionMapping: {
|
|
652
|
+
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
|
|
653
|
+
mrzTypes: backMrzType ? [backMrzType] : []
|
|
654
|
+
},
|
|
616
655
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
617
|
-
code: countryData?.code || '',
|
|
618
|
-
|
|
656
|
+
code: countryData?.code || '',
|
|
657
|
+
currentSide,
|
|
658
|
+
templatePath,
|
|
659
|
+
mrzType: backMrzType,
|
|
660
|
+
templateResponse,
|
|
661
|
+
issuingSubdivision: issuingSubdivision,
|
|
662
|
+
frontTemplatePath: frontTemplatePath
|
|
619
663
|
}, env);
|
|
620
664
|
}
|
|
621
|
-
|
|
665
|
+
|
|
622
666
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
623
667
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
624
|
-
|
|
668
|
+
|
|
625
669
|
if (isBarcodeRequired()) {
|
|
626
670
|
const barcodeData = scannedBarcodeRef.current;
|
|
627
671
|
const docTypeStr = selectedDocumentType!.type;
|
|
628
672
|
const isHIC = docTypeStr === 'health_insurance_card';
|
|
629
673
|
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
|
|
674
|
+
|
|
632
675
|
if (isValidScan) {
|
|
633
676
|
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
634
677
|
let parsedDictionary;
|
|
@@ -638,14 +681,15 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
638
681
|
const token = await authentification();
|
|
639
682
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
640
683
|
}
|
|
641
|
-
|
|
684
|
+
|
|
642
685
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
643
686
|
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
644
687
|
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
645
688
|
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
646
689
|
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
647
690
|
};
|
|
648
|
-
|
|
691
|
+
|
|
692
|
+
clearTimeout(watchdog);
|
|
649
693
|
setSilentCaptureResult(verifiedResult);
|
|
650
694
|
if (bbox) {
|
|
651
695
|
const safeBbox = bbox as IBbox;
|
|
@@ -654,41 +698,60 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
654
698
|
await autoCapture(safeCapturePath!, verifiedResult);
|
|
655
699
|
return;
|
|
656
700
|
}
|
|
657
|
-
|
|
658
|
-
// If validation passed but we still need the barcode, trigger the UI phase!
|
|
701
|
+
|
|
659
702
|
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
703
|
+
clearTimeout(watchdog);
|
|
660
704
|
setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
|
|
661
705
|
setScanPhase('barcode');
|
|
662
706
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
663
707
|
return;
|
|
664
708
|
}
|
|
665
|
-
|
|
666
|
-
// 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
|
|
709
|
+
|
|
667
710
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
668
|
-
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
669
|
-
country: countryData?.code, documentType: selectedDocumentType.type,
|
|
711
|
+
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
712
|
+
isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type,
|
|
713
|
+
region: issuingSubdivision
|
|
670
714
|
};
|
|
671
715
|
|
|
716
|
+
clearTimeout(watchdog);
|
|
672
717
|
setSilentCaptureResult(verifiedResult);
|
|
673
718
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
674
719
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
675
|
-
|
|
720
|
+
|
|
676
721
|
} catch (error: any) {
|
|
722
|
+
clearTimeout(watchdog);
|
|
677
723
|
console.log("Backend Verification Error:", error);
|
|
678
|
-
const rawMessage = (error?.message || '').toLowerCase();
|
|
679
|
-
let userFriendlyMessage ='Verification failed. Please try again.';
|
|
680
724
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
725
|
+
const isNetworkError =
|
|
726
|
+
!error.response ||
|
|
727
|
+
error.message === 'Network Error' ||
|
|
728
|
+
error.code === 'ECONNABORTED' ||
|
|
729
|
+
error.message?.includes('Network');
|
|
730
|
+
|
|
731
|
+
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
732
|
+
|
|
733
|
+
if (isNetworkError) {
|
|
734
|
+
userFriendlyMessage = state.currentLanguage === 'en'
|
|
735
|
+
? 'Cannot connect to verification server. Please check your internet connection.'
|
|
736
|
+
: 'Impossible de se connecter au serveur de vérification. Veuillez vérifier votre connexion internet.';
|
|
737
|
+
|
|
738
|
+
Alert.alert(
|
|
739
|
+
state.currentLanguage === 'en' ? "Connection Failed" : "Échec de connexion",
|
|
740
|
+
userFriendlyMessage
|
|
741
|
+
);
|
|
742
|
+
} else {
|
|
743
|
+
const rawMessage = (error?.message || '').toLowerCase();
|
|
744
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
745
|
+
userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
|
|
746
|
+
} else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
747
|
+
userFriendlyMessage = 'The document does not match your selected country.';
|
|
748
|
+
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
749
|
+
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
750
|
+
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
751
|
+
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
752
|
+
}
|
|
689
753
|
}
|
|
690
|
-
|
|
691
|
-
// The error is thrown here, completely blocking the switch to 'barcode' phase
|
|
754
|
+
|
|
692
755
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
693
756
|
}
|
|
694
757
|
}
|