@sanctum-key/react-native-sdk 1.0.31 → 1.0.33
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 +6 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +21 -8
- 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 +53 -48
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/package.json +2 -1
- package/src/components/EnhancedCameraView.tsx +28 -9
- package/src/components/KYCElements/IDCardCapture.tsx +72 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanctum-key/react-native-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.33",
|
|
4
4
|
"description": "Sanctum Key React Native SDK",
|
|
5
5
|
"main": "build/src/index.js",
|
|
6
6
|
"types": "build/src/index.d.ts",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"i18n-js": "4.5.1",
|
|
55
55
|
"iconsax-react-nativejs": "^0.0.8",
|
|
56
56
|
"react-native-qrcode-svg": "^6.3.21",
|
|
57
|
+
"react-native-worklets-core": "^1.6.3",
|
|
57
58
|
"zustand": "^5.0.8"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { View, StyleSheet, Text, Platform } from 'react-native';
|
|
2
|
+
import { View, StyleSheet, Text, Platform, AppState, AppStateStatus } from 'react-native';
|
|
3
3
|
import { Camera, CodeType, useCameraDevice, useCodeScanner } from 'react-native-vision-camera';
|
|
4
4
|
import VisionCameraModule from '../modules/camera/VisionCameraModule';
|
|
5
5
|
import { useI18n } from '../hooks/useI18n';
|
|
6
6
|
import { EnhancedCameraViewProps } from './OverLay/type';
|
|
7
7
|
import { WebBarcodeScanner } from './WebBarcodeScanner';
|
|
8
8
|
|
|
9
|
-
export interface ExtendedCameraProps extends EnhancedCameraViewProps {
|
|
9
|
+
export interface ExtendedCameraProps extends Omit<EnhancedCameraViewProps, 'onError'> {
|
|
10
10
|
isScanningBarcode?: boolean;
|
|
11
11
|
onBarcodeScanned?: (data: string) => void;
|
|
12
12
|
barcodeTypes?: CodeType[];
|
|
13
|
+
onError?: (error: { message: string; name?: string; code?: string }) => void;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export const EnhancedCameraView: React.FC<ExtendedCameraProps> = ({
|
|
@@ -41,22 +42,35 @@ export const EnhancedCameraView: React.FC<ExtendedCameraProps> = ({
|
|
|
41
42
|
const scannedBarcodeRef = useRef<string | null>(null);
|
|
42
43
|
const containerRef = useRef<any>(null);
|
|
43
44
|
|
|
45
|
+
const [appState, setAppState] = useState<AppStateStatus>(AppState.currentState);
|
|
46
|
+
|
|
44
47
|
const device = useCameraDevice(cameraType);
|
|
45
48
|
|
|
46
49
|
useEffect(() => {
|
|
47
50
|
isProcessingRef.current = isProcessing;
|
|
48
51
|
}, [isProcessing]);
|
|
49
52
|
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const subscription = AppState.addEventListener('change', nextAppState => {
|
|
55
|
+
setAppState(nextAppState);
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
subscription.remove();
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
50
62
|
const checkPermissions = async () => {
|
|
51
63
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
let cameraStatus = Camera.getCameraPermissionStatus();
|
|
65
|
+
|
|
66
|
+
if (cameraStatus !== 'granted') {
|
|
67
|
+
cameraStatus = await Camera.requestCameraPermission();
|
|
68
|
+
|
|
69
|
+
if (cameraStatus !== 'granted') {
|
|
56
70
|
setHasPermission(false);
|
|
57
71
|
onError?.({
|
|
58
72
|
message: t('camera.permissionRequired'),
|
|
59
|
-
name: 'NotAllowedError',
|
|
73
|
+
name: 'NotAllowedError',
|
|
60
74
|
code: 'permission-denied'
|
|
61
75
|
});
|
|
62
76
|
return;
|
|
@@ -112,7 +126,12 @@ export const EnhancedCameraView: React.FC<ExtendedCameraProps> = ({
|
|
|
112
126
|
isCapturingRef.current = true;
|
|
113
127
|
const photo = await camera.current.takePhoto({
|
|
114
128
|
enableShutterSound: false,
|
|
115
|
-
flash: 'off'
|
|
129
|
+
flash: 'off',
|
|
130
|
+
|
|
131
|
+
...(Platform.OS === 'ios' ? {
|
|
132
|
+
photoCodec: 'jpeg',
|
|
133
|
+
qualityPrioritization: 'speed',
|
|
134
|
+
} : {})
|
|
116
135
|
});
|
|
117
136
|
|
|
118
137
|
const result = await VisionCameraModule.processPhotoResult(photo);
|
|
@@ -210,7 +229,7 @@ export const EnhancedCameraView: React.FC<ExtendedCameraProps> = ({
|
|
|
210
229
|
style={StyleSheet.absoluteFill}
|
|
211
230
|
device={device}
|
|
212
231
|
resizeMode="cover"
|
|
213
|
-
isActive={showCamera && !isProcessing}
|
|
232
|
+
isActive={showCamera && !isProcessing && appState === 'active'}
|
|
214
233
|
photo={true}
|
|
215
234
|
video={false}
|
|
216
235
|
audio={false}
|
|
@@ -48,6 +48,34 @@ const SHORT_TO_LONG_REGION: Record<string, string> = {
|
|
|
48
48
|
"SK": "Saskatchewan", "YT": "Yukon"
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// 🚨 Isolates the 1-second re-renders away from the Camera!
|
|
52
|
+
const ExpiringCodeTimer = ({ initialCode, language }: { initialCode: string, language: string }) => {
|
|
53
|
+
const [timeLeft, setTimeLeft] = useState(300);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (timeLeft <= 0) return;
|
|
57
|
+
const timer = setInterval(() => setTimeLeft((prev) => prev - 1), 1000);
|
|
58
|
+
return () => clearInterval(timer);
|
|
59
|
+
}, [timeLeft]);
|
|
60
|
+
|
|
61
|
+
const formatTime = (seconds: number) => {
|
|
62
|
+
const m = Math.floor(seconds / 60);
|
|
63
|
+
const s = seconds % 60;
|
|
64
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (timeLeft <= 0) return null;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View style={styles.activeCodeContainer}>
|
|
71
|
+
<Text style={styles.activeCodeText}>{initialCode}</Text>
|
|
72
|
+
<Text style={styles.timerText}>
|
|
73
|
+
{language === 'en' ? `Expires in ${formatTime(timeLeft)}` : `Expire dans ${formatTime(timeLeft)}`}
|
|
74
|
+
</Text>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
51
79
|
export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
|
|
52
80
|
const { t, locale } = useI18n();
|
|
53
81
|
const [showCamera, setShowCamera] = useState(false);
|
|
@@ -69,54 +97,32 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
69
97
|
|
|
70
98
|
const scannedBarcodeRef = useRef<string | null>(null);
|
|
71
99
|
|
|
72
|
-
// 🚨 NEW: State for the expiring link code
|
|
73
100
|
const [generatedCode, setGeneratedCode] = useState<string | null>(null);
|
|
74
101
|
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
|
75
|
-
const [codeTimeLeft, setCodeTimeLeft] = useState(0);
|
|
76
|
-
|
|
77
|
-
// 🚨 NEW: Countdown Timer Effect
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (codeTimeLeft <= 0) {
|
|
80
|
-
if (generatedCode) setGeneratedCode(null); // Clear the code when timer hits 0
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const timer = setInterval(() => {
|
|
84
|
-
setCodeTimeLeft((prev) => prev - 1);
|
|
85
|
-
}, 1000);
|
|
86
|
-
return () => clearInterval(timer);
|
|
87
|
-
}, [codeTimeLeft, generatedCode]);
|
|
88
102
|
|
|
89
|
-
// 🚨 NEW: Formatting helper
|
|
90
|
-
const formatTime = (seconds: number) => {
|
|
91
|
-
const m = Math.floor(seconds / 60);
|
|
92
|
-
const s = seconds % 60;
|
|
93
|
-
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
94
|
-
};
|
|
95
103
|
|
|
96
104
|
const handleGenerateCode = async () => {
|
|
97
105
|
if (isGeneratingCode) return;
|
|
98
106
|
setIsGeneratingCode(true);
|
|
107
|
+
|
|
99
108
|
try {
|
|
100
|
-
const apiKey = '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';
|
|
101
|
-
|
|
102
109
|
if (!apiKey) {
|
|
103
110
|
showAlert('Error', 'API Key is missing.');
|
|
104
111
|
setIsGeneratingCode(false);
|
|
105
112
|
return;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
//
|
|
115
|
+
// Pass the dynamic apiKey to your service
|
|
109
116
|
const res = await kycService.generateReferralCode(
|
|
110
117
|
{ session_id: state.session.session_id },
|
|
111
118
|
apiKey
|
|
112
119
|
);
|
|
113
|
-
|
|
120
|
+
|
|
114
121
|
// Extract the code from the response
|
|
115
122
|
const newCode = res.code || res.referral_code || res?.data?.code || res?.data?.referral_code;
|
|
116
123
|
|
|
117
124
|
if (newCode) {
|
|
118
125
|
setGeneratedCode(newCode);
|
|
119
|
-
setCodeTimeLeft(300); // 300 seconds = 5 minutes
|
|
120
126
|
} else {
|
|
121
127
|
showAlert('Error', 'Failed to parse code from server response.');
|
|
122
128
|
}
|
|
@@ -139,7 +145,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
139
145
|
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
140
146
|
};
|
|
141
147
|
|
|
142
|
-
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
148
|
+
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
143
149
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
144
150
|
|
|
145
151
|
const getLocalizedText = (text: LocalizedText | Record<string, LocalizedText>): string => {
|
|
@@ -382,7 +388,12 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
382
388
|
const autoCapture = async (capturePath: string, verified: ISilentCaptureResult & { region?: string }) => {
|
|
383
389
|
if (isProcessingCapture) return;
|
|
384
390
|
setIsProcessingCapture(true);
|
|
385
|
-
|
|
391
|
+
|
|
392
|
+
const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://'))
|
|
393
|
+
? `file://${capturePath}`
|
|
394
|
+
: capturePath;
|
|
395
|
+
|
|
396
|
+
setProcessingImagePath(safeCapturePath);
|
|
386
397
|
|
|
387
398
|
try {
|
|
388
399
|
let imagePathForUpload = capturePath;
|
|
@@ -394,13 +405,17 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
394
405
|
};
|
|
395
406
|
|
|
396
407
|
try {
|
|
397
|
-
imagePathForUpload = await cropImageWithBBoxWithTolerance(
|
|
408
|
+
imagePathForUpload = await cropImageWithBBoxWithTolerance(safeCapturePath, uiCropBbox, 0.06);
|
|
398
409
|
} catch (e) {
|
|
399
410
|
console.warn("Crop failed, falling back to original image", e);
|
|
400
|
-
imagePathForUpload =
|
|
411
|
+
imagePathForUpload = safeCapturePath;
|
|
401
412
|
}
|
|
402
413
|
|
|
403
|
-
const
|
|
414
|
+
const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://'))
|
|
415
|
+
? `file://${imagePathForUpload}`
|
|
416
|
+
: imagePathForUpload;
|
|
417
|
+
|
|
418
|
+
const base64 = await pathToBase64(safeCroppedPath);
|
|
404
419
|
const newImages = {
|
|
405
420
|
...capturedImages,
|
|
406
421
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
@@ -543,11 +558,15 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
543
558
|
|
|
544
559
|
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
545
560
|
|
|
561
|
+
const safeCapturePath = (Platform.OS === 'ios' && !result.path.startsWith('file://'))
|
|
562
|
+
? `file://${result.path}`
|
|
563
|
+
: result.path;
|
|
564
|
+
|
|
546
565
|
try {
|
|
547
566
|
// 1. Check & Crop the Template visually first
|
|
548
567
|
if (!templatePath) {
|
|
549
568
|
const templateType = await checkTemplateType({
|
|
550
|
-
path:
|
|
569
|
+
path: safeCapturePath,
|
|
551
570
|
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
552
571
|
docRegion: countryData?.code || "",
|
|
553
572
|
postfix: currentSide,
|
|
@@ -564,8 +583,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
564
583
|
return;
|
|
565
584
|
}
|
|
566
585
|
try {
|
|
567
|
-
const crop = await cropByObb(
|
|
568
|
-
templateBbox = crop.bbox;
|
|
586
|
+
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb); templateBbox = crop.bbox;
|
|
569
587
|
} catch {}
|
|
570
588
|
}
|
|
571
589
|
}
|
|
@@ -579,21 +597,21 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
579
597
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
580
598
|
|
|
581
599
|
verificationRes = await frontVerification({
|
|
582
|
-
path:
|
|
600
|
+
path: safeCapturePath,
|
|
583
601
|
regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
|
|
584
602
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
585
603
|
code: countryData?.code || '', currentSide, templatePath, mrzType,
|
|
586
604
|
issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode
|
|
587
605
|
}, env);
|
|
588
606
|
} else {
|
|
589
|
-
logger.log("Attempting backVerification with:", { path:
|
|
607
|
+
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
590
608
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
591
609
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
592
610
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
593
611
|
|
|
594
612
|
// This validates if the back matches the front template!
|
|
595
613
|
verificationRes = await backVerification({
|
|
596
|
-
path:
|
|
614
|
+
path: safeCapturePath,
|
|
597
615
|
regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
|
|
598
616
|
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
599
617
|
code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse,
|
|
@@ -622,7 +640,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
622
640
|
}
|
|
623
641
|
|
|
624
642
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
625
|
-
path:
|
|
643
|
+
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
626
644
|
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
627
645
|
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
628
646
|
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
@@ -633,13 +651,13 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
633
651
|
const safeBbox = bbox as IBbox;
|
|
634
652
|
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
635
653
|
}
|
|
636
|
-
await autoCapture(
|
|
654
|
+
await autoCapture(safeCapturePath!, verifiedResult);
|
|
637
655
|
return;
|
|
638
656
|
}
|
|
639
657
|
|
|
640
658
|
// If validation passed but we still need the barcode, trigger the UI phase!
|
|
641
659
|
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
642
|
-
setPendingCAImage({ path:
|
|
660
|
+
setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
|
|
643
661
|
setScanPhase('barcode');
|
|
644
662
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
645
663
|
return;
|
|
@@ -647,13 +665,13 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
647
665
|
|
|
648
666
|
// 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
|
|
649
667
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
650
|
-
path:
|
|
668
|
+
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
|
|
651
669
|
country: countryData?.code, documentType: selectedDocumentType.type, region: issuingSubdivision
|
|
652
670
|
};
|
|
653
671
|
|
|
654
672
|
setSilentCaptureResult(verifiedResult);
|
|
655
673
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
656
|
-
await autoCapture(
|
|
674
|
+
await autoCapture(safeCapturePath, verifiedResult);
|
|
657
675
|
|
|
658
676
|
} catch (error: any) {
|
|
659
677
|
console.log("Backend Verification Error:", error);
|
|
@@ -1114,28 +1132,19 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
1114
1132
|
{state.currentLanguage === 'en' ? 'Use code below to proceed on the Sanctum Key App.' : 'Vous avez déjà l\'application ? Liez-la :'}
|
|
1115
1133
|
</Text>
|
|
1116
1134
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
<
|
|
1121
|
-
{
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
{isGeneratingCode ? (
|
|
1131
|
-
<ActivityIndicator size="small" color="#0F172A" />
|
|
1132
|
-
) : (
|
|
1133
|
-
<Text style={styles.generateCodeButtonText}>
|
|
1134
|
-
{state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
|
|
1135
|
-
</Text>
|
|
1136
|
-
)}
|
|
1137
|
-
</TouchableOpacity>
|
|
1138
|
-
)}
|
|
1135
|
+
{generatedCode ? (
|
|
1136
|
+
<ExpiringCodeTimer initialCode={generatedCode} language={state.currentLanguage} />
|
|
1137
|
+
) : (
|
|
1138
|
+
<TouchableOpacity style={styles.generateCodeButton} onPress={handleGenerateCode} disabled={isGeneratingCode}>
|
|
1139
|
+
{isGeneratingCode ? (
|
|
1140
|
+
<ActivityIndicator size="small" color="#0F172A" />
|
|
1141
|
+
) : (
|
|
1142
|
+
<Text style={styles.generateCodeButtonText}>
|
|
1143
|
+
{state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
|
|
1144
|
+
</Text>
|
|
1145
|
+
)}
|
|
1146
|
+
</TouchableOpacity>
|
|
1147
|
+
)}
|
|
1139
1148
|
</View>
|
|
1140
1149
|
|
|
1141
1150
|
{isWebScanningRestricted && (
|