@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanctum-key/react-native-sdk",
3
- "version": "1.0.31",
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
- const hasAllPermissions = await VisionCameraModule.hasAllPermissions();
53
- if (!hasAllPermissions) {
54
- const granted = await VisionCameraModule.requestAllPermissions();
55
- if (!granted) {
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', // Emulate web standard for easy catching
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
- // 🚨 Pass the apiKey as the strictly required second parameter
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
- setProcessingImagePath(capturePath);
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(capturePath, uiCropBbox, 0.06);
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 = capturePath;
411
+ imagePathForUpload = safeCapturePath;
401
412
  }
402
413
 
403
- const base64 = await pathToBase64(imagePathForUpload);
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: result.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(result.path, (templateType as any).card_obb);
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: result.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: result.path, templatePath, side: currentSide });
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: result.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: result.path, templatePath: templatePath, bbox: bbox, success: true,
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(result.path!, verifiedResult);
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: result.path!, templateResponse, templatePath, bbox });
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: result.path, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
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(result.path, verifiedResult);
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
- {generatedCode && codeTimeLeft > 0 ? (
1118
- <View style={styles.activeCodeContainer}>
1119
- <Text style={styles.activeCodeText}>{generatedCode}</Text>
1120
- <Text style={styles.timerText}>
1121
- {state.currentLanguage === 'en' ? `Expires in ${formatTime(codeTimeLeft)}` : `Expire dans ${formatTime(codeTimeLeft)}`}
1122
- </Text>
1123
- </View>
1124
- ) : (
1125
- <TouchableOpacity
1126
- style={styles.generateCodeButton}
1127
- onPress={handleGenerateCode}
1128
- disabled={isGeneratingCode}
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 && (