@sanctum-key/react-native-sdk 1.0.36 → 1.0.38
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/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +207 -462
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +304 -731
|
@@ -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
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -14,7 +14,6 @@ import pathToBase64 from '../../utils/pathToBase64';
|
|
|
14
14
|
import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
|
|
15
15
|
import REGION_MAPPING from '../../config/region_mapping.json';
|
|
16
16
|
import QRCode from 'react-native-qrcode-svg';
|
|
17
|
-
import { logger } from '../../utils/logger';
|
|
18
17
|
import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
19
18
|
import kycService, { authentification } from '../../modules/api/KYCService';
|
|
20
19
|
const ISO_TO_COUNTRY_NAME = {
|
|
@@ -23,13 +22,11 @@ const ISO_TO_COUNTRY_NAME = {
|
|
|
23
22
|
'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
|
|
24
23
|
};
|
|
25
24
|
const SHORT_TO_LONG_REGION = {
|
|
26
|
-
"AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
|
|
25
|
+
"AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba", "NB": "New Brunswick",
|
|
26
|
+
"NL": "Newfoundland and Labrador", "NT": "Northwest Territories", "NS": "Nova Scotia",
|
|
27
|
+
"NU": "Nunavut", "ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
|
|
30
28
|
"SK": "Saskatchewan", "YT": "Yukon"
|
|
31
29
|
};
|
|
32
|
-
// 🚨 Isolates the 1-second re-renders away from the Camera!
|
|
33
30
|
const ExpiringCodeTimer = ({ initialCode, language }) => {
|
|
34
31
|
const [timeLeft, setTimeLeft] = useState(300);
|
|
35
32
|
useEffect(() => {
|
|
@@ -70,12 +67,48 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
70
67
|
const [scanPhase, setScanPhase] = useState('image');
|
|
71
68
|
const [pendingCAImage, setPendingCAImage] = useState(null);
|
|
72
69
|
const scannedBarcodeRef = useRef(null);
|
|
73
|
-
|
|
70
|
+
const [generatedCode, setGeneratedCode] = useState(null);
|
|
71
|
+
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
|
72
|
+
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
73
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
74
|
+
console.log(currentUrl);
|
|
75
|
+
const countrySelectionData = useMemo(() => {
|
|
76
|
+
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
77
|
+
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
78
|
+
}, [state.template.components, state.componentData]);
|
|
79
|
+
const documentTypeMapping = {
|
|
80
|
+
'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
|
|
81
|
+
'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
82
|
+
'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
|
|
83
|
+
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
84
|
+
};
|
|
85
|
+
const selectedDocumentType = useMemo(() => {
|
|
86
|
+
if (!countrySelectionData?.documentType)
|
|
87
|
+
return null;
|
|
88
|
+
const backendDocType = countrySelectionData.documentType;
|
|
89
|
+
const mappedType = documentTypeMapping[backendDocType] || backendDocType;
|
|
90
|
+
return { type: mappedType, region: countrySelectionData.region || 'root' };
|
|
91
|
+
}, [countrySelectionData, documentTypeMapping]);
|
|
92
|
+
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
93
|
+
// ==========================================
|
|
94
|
+
// 🚨 CRITICAL FIX: REF MIRRORS FOR IOS BRIDGE
|
|
95
|
+
// Bypass stale native closures by mirroring all state into refs
|
|
96
|
+
// ==========================================
|
|
74
97
|
const isAnalyzingRef = useRef(false);
|
|
75
98
|
const isProcessingCaptureRef = useRef(false);
|
|
76
99
|
const scanPhaseRef = useRef('image');
|
|
77
|
-
const
|
|
78
|
-
const
|
|
100
|
+
const currentSideRef = useRef(currentSide);
|
|
101
|
+
const capturedImagesRef = useRef(capturedImages);
|
|
102
|
+
const selectedDocTypeRef = useRef(selectedDocumentType);
|
|
103
|
+
const countryDataRef = useRef(countryData);
|
|
104
|
+
useEffect(() => { isAnalyzingRef.current = silentCaptureResult.isAnalyzing ?? false; }, [silentCaptureResult.isAnalyzing]);
|
|
105
|
+
useEffect(() => { isProcessingCaptureRef.current = isProcessingCapture; }, [isProcessingCapture]);
|
|
106
|
+
useEffect(() => { scanPhaseRef.current = scanPhase; }, [scanPhase]);
|
|
107
|
+
useEffect(() => { currentSideRef.current = currentSide; }, [currentSide]);
|
|
108
|
+
useEffect(() => { capturedImagesRef.current = capturedImages; }, [capturedImages]);
|
|
109
|
+
useEffect(() => { selectedDocTypeRef.current = selectedDocumentType; }, [selectedDocumentType]);
|
|
110
|
+
useEffect(() => { countryDataRef.current = countryData; }, [countryData]);
|
|
111
|
+
// ==========================================
|
|
79
112
|
const handleGenerateCode = async () => {
|
|
80
113
|
if (isGeneratingCode)
|
|
81
114
|
return;
|
|
@@ -86,16 +119,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
86
119
|
setIsGeneratingCode(false);
|
|
87
120
|
return;
|
|
88
121
|
}
|
|
89
|
-
// Pass the dynamic apiKey to your service
|
|
90
122
|
const res = await kycService.generateReferralCode({ session_id: state.session.session_id }, apiKey);
|
|
91
|
-
// Extract the code from the response
|
|
92
123
|
const newCode = res.code || res.referral_code || res?.data?.code || res?.data?.referral_code;
|
|
93
|
-
if (newCode)
|
|
124
|
+
if (newCode)
|
|
94
125
|
setGeneratedCode(newCode);
|
|
95
|
-
|
|
96
|
-
else {
|
|
126
|
+
else
|
|
97
127
|
showAlert('Error', 'Failed to parse code from server response.');
|
|
98
|
-
}
|
|
99
128
|
}
|
|
100
129
|
catch (error) {
|
|
101
130
|
showAlert('Error', error.message || 'Failed to generate code');
|
|
@@ -107,50 +136,19 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
107
136
|
useEffect(() => {
|
|
108
137
|
if (typeof window !== 'undefined' && window.location)
|
|
109
138
|
setCurrentUrl(window.location.href);
|
|
110
|
-
console.log(currentUrl);
|
|
111
139
|
}, []);
|
|
112
|
-
const documentTypeMapping = {
|
|
113
|
-
'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
|
|
114
|
-
'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
115
|
-
'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
|
|
116
|
-
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
117
|
-
};
|
|
118
|
-
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
119
|
-
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
120
140
|
const getLocalizedText = (text) => {
|
|
121
141
|
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
|
|
122
142
|
return text[currentSide][locale] || '';
|
|
123
143
|
return "";
|
|
124
144
|
};
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
127
|
-
const baseUrl = window.location.origin + window.location.pathname;
|
|
128
|
-
const handoffUrl = `${baseUrl}?session_id=${state.session.session_id}&resume=true`;
|
|
129
|
-
setCurrentUrl(handoffUrl);
|
|
130
|
-
}
|
|
131
|
-
}, [state.session.session_id]);
|
|
132
|
-
const countrySelectionData = useMemo(() => {
|
|
133
|
-
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
134
|
-
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
135
|
-
}, [state.template.components, state.componentData]);
|
|
136
|
-
const selectedDocumentType = useMemo(() => {
|
|
137
|
-
if (!countrySelectionData?.documentType)
|
|
138
|
-
return null;
|
|
139
|
-
const backendDocType = countrySelectionData.documentType;
|
|
140
|
-
const mappedType = documentTypeMapping[backendDocType] || backendDocType;
|
|
141
|
-
return { type: mappedType, region: countrySelectionData.region || 'root' };
|
|
142
|
-
}, [countrySelectionData, documentTypeMapping]);
|
|
143
|
-
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
144
145
|
const isWebScanningRestricted = useMemo(() => {
|
|
145
146
|
if (Platform.OS !== 'web')
|
|
146
147
|
return false;
|
|
147
148
|
if (countryData?.code !== 'CA')
|
|
148
149
|
return false;
|
|
149
150
|
const docType = selectedDocumentType?.type;
|
|
150
|
-
return docType === 'drivers_licence' ||
|
|
151
|
-
docType === 'health_insurance_card' ||
|
|
152
|
-
docType === 'national_id' ||
|
|
153
|
-
docType === 'identity_card';
|
|
151
|
+
return docType === 'drivers_licence' || docType === 'health_insurance_card' || docType === 'national_id' || docType === 'identity_card';
|
|
154
152
|
}, [countryData?.code, selectedDocumentType?.type]);
|
|
155
153
|
const targetBarcodeTypes = useMemo(() => {
|
|
156
154
|
const docType = selectedDocumentType?.type;
|
|
@@ -164,25 +162,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
164
162
|
setScanPhase('image');
|
|
165
163
|
setPendingCAImage(null);
|
|
166
164
|
scannedBarcodeRef.current = null;
|
|
167
|
-
setCapturedImages((prev) => {
|
|
168
|
-
const newState = { ...prev };
|
|
169
|
-
delete newState[currentSide];
|
|
170
|
-
return newState;
|
|
171
|
-
});
|
|
165
|
+
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[currentSide]; return newState; });
|
|
172
166
|
if (value) {
|
|
173
167
|
const newValue = { ...value };
|
|
174
168
|
delete newValue[currentSide];
|
|
175
169
|
onValueChange(newValue);
|
|
176
170
|
}
|
|
177
|
-
setSilentCaptureResult({
|
|
178
|
-
success: false,
|
|
179
|
-
isAnalyzing: false,
|
|
180
|
-
path: '',
|
|
181
|
-
error: '',
|
|
182
|
-
templatePath: '',
|
|
183
|
-
mrz: '',
|
|
184
|
-
bbox: undefined
|
|
185
|
-
});
|
|
171
|
+
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
186
172
|
}
|
|
187
173
|
setTimeout(() => {
|
|
188
174
|
setCameraKey(prev => prev + 1);
|
|
@@ -194,35 +180,19 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
194
180
|
refreshCamera();
|
|
195
181
|
};
|
|
196
182
|
useEffect(() => {
|
|
197
|
-
if (value && Object.keys(value).length > 0) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
setSilentCaptureResult(prev => ({
|
|
204
|
-
...prev,
|
|
205
|
-
path: currentImageData.dir,
|
|
206
|
-
success: true,
|
|
207
|
-
isAnalyzing: false,
|
|
208
|
-
mrz: currentImageData.mrz || '',
|
|
209
|
-
templatePath: currentImageData.templatePath || '',
|
|
210
|
-
}));
|
|
211
|
-
}
|
|
183
|
+
if (value && Object.keys(value).length > 0 && JSON.stringify(value) !== JSON.stringify(capturedImages)) {
|
|
184
|
+
const updatedImages = value;
|
|
185
|
+
setCapturedImages(updatedImages);
|
|
186
|
+
const currentImageData = updatedImages[currentSide];
|
|
187
|
+
if (currentImageData?.dir) {
|
|
188
|
+
setSilentCaptureResult(prev => ({ ...prev, path: currentImageData.dir, success: true, isAnalyzing: false, mrz: currentImageData.mrz || '', templatePath: currentImageData.templatePath || '' }));
|
|
212
189
|
}
|
|
213
190
|
}
|
|
214
191
|
}, [value, currentSide]);
|
|
215
|
-
// Keep refs in sync with state so handleSilentCapture always reads current values
|
|
216
|
-
useEffect(() => { isAnalyzingRef.current = silentCaptureResult.isAnalyzing ?? false; }, [silentCaptureResult.isAnalyzing]);
|
|
217
|
-
useEffect(() => { isProcessingCaptureRef.current = isProcessingCapture; }, [isProcessingCapture]);
|
|
218
|
-
useEffect(() => { scanPhaseRef.current = scanPhase; }, [scanPhase]);
|
|
219
192
|
const cameraConfig = useMemo(() => {
|
|
220
|
-
let instructions = selectedDocumentType
|
|
221
|
-
|
|
222
|
-
: getLocalizedText(component.instructions);
|
|
223
|
-
if (scanPhase === 'barcode') {
|
|
193
|
+
let instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
|
|
194
|
+
if (scanPhase === 'barcode')
|
|
224
195
|
instructions = '';
|
|
225
|
-
}
|
|
226
196
|
const isLandscape = screenWidth > screenHeight;
|
|
227
197
|
let xMin, xMax, yMin, yMax;
|
|
228
198
|
if (Platform.OS === 'web') {
|
|
@@ -243,20 +213,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
243
213
|
yMin = 34;
|
|
244
214
|
yMax = 66;
|
|
245
215
|
}
|
|
246
|
-
const platformBbox = {
|
|
247
|
-
|
|
248
|
-
yMin,
|
|
249
|
-
xMax,
|
|
250
|
-
yMax,
|
|
251
|
-
borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60',
|
|
252
|
-
borderWidth: Platform.OS === 'web' ? 2 : 3,
|
|
253
|
-
cornerRadius: 8
|
|
254
|
-
};
|
|
255
|
-
return {
|
|
256
|
-
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
257
|
-
flashMode: 'auto',
|
|
258
|
-
overlay: { guideText: instructions, bbox: platformBbox }
|
|
259
|
-
};
|
|
216
|
+
const platformBbox = { xMin, yMin, xMax, yMax, borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
|
|
217
|
+
return { cameraType: Platform.OS === 'web' ? cameraType : 'back', flashMode: 'auto', overlay: { guideText: instructions, bbox: platformBbox } };
|
|
260
218
|
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight, scanPhase]);
|
|
261
219
|
const retakePicture = (sideToRetake) => {
|
|
262
220
|
setIsProcessingCapture(false);
|
|
@@ -268,28 +226,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
268
226
|
setShowCamera(true);
|
|
269
227
|
refreshCamera();
|
|
270
228
|
actions.showCustomStepper(false);
|
|
271
|
-
setCapturedImages((prev) => {
|
|
272
|
-
const newState = { ...prev };
|
|
273
|
-
delete newState[sideToRetake];
|
|
274
|
-
return newState;
|
|
275
|
-
});
|
|
229
|
+
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
276
230
|
if (value) {
|
|
277
231
|
const newValue = { ...value };
|
|
278
232
|
delete newValue[sideToRetake];
|
|
279
233
|
onValueChange(newValue);
|
|
280
234
|
}
|
|
281
235
|
};
|
|
282
|
-
const getCurrentSideVerification = (
|
|
283
|
-
const rawDocType =
|
|
236
|
+
const getCurrentSideVerification = (side, countryKey, cData) => {
|
|
237
|
+
const rawDocType = cData?.documentType;
|
|
284
238
|
if (!rawDocType || !countryKey)
|
|
285
239
|
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
286
|
-
const jsonDocMap = {
|
|
287
|
-
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
288
|
-
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
289
|
-
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
290
|
-
};
|
|
240
|
+
const jsonDocMap = { 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card' };
|
|
291
241
|
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
292
|
-
const rawCountryName = ISO_TO_COUNTRY_NAME[
|
|
242
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[cData?.code || ''] || cData?.code || countryKey;
|
|
293
243
|
const baseMapping = REGION_MAPPING.regionMapping || REGION_MAPPING;
|
|
294
244
|
let countryMapping = baseMapping[rawCountryName];
|
|
295
245
|
if (!countryMapping) {
|
|
@@ -301,13 +251,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
301
251
|
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
302
252
|
const authMethod = [];
|
|
303
253
|
const mrzTypes = [];
|
|
304
|
-
const rawRegionKey =
|
|
254
|
+
const rawRegionKey = cData.region?.trim()?.length > 0 ? cData.region.trim() : 'root';
|
|
305
255
|
const key = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
306
256
|
const regionMapping = countryMapping[searchDocType];
|
|
307
257
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
308
258
|
regionMapping[key].forEach((item) => {
|
|
309
|
-
if (item[
|
|
310
|
-
authMethod.push(item[
|
|
259
|
+
if (item[side])
|
|
260
|
+
authMethod.push(item[side]);
|
|
311
261
|
if (item?.mrz_type)
|
|
312
262
|
mrzTypes.push(item?.mrz_type);
|
|
313
263
|
});
|
|
@@ -338,18 +288,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
338
288
|
if (isProcessingCapture)
|
|
339
289
|
return;
|
|
340
290
|
setIsProcessingCapture(true);
|
|
341
|
-
const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://'))
|
|
342
|
-
? `file://${capturePath}`
|
|
343
|
-
: capturePath;
|
|
291
|
+
const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://')) ? `file://${capturePath}` : capturePath;
|
|
344
292
|
setProcessingImagePath(safeCapturePath);
|
|
345
293
|
try {
|
|
346
294
|
let imagePathForUpload = capturePath;
|
|
347
295
|
const overlayBbox = cameraConfig.overlay.bbox;
|
|
348
|
-
const uiCropBbox = {
|
|
349
|
-
minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
|
|
350
|
-
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
351
|
-
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
352
|
-
};
|
|
296
|
+
const uiCropBbox = { minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100, width: (overlayBbox.xMax - overlayBbox.xMin) / 100, height: (overlayBbox.yMax - overlayBbox.yMin) / 100 };
|
|
353
297
|
try {
|
|
354
298
|
imagePathForUpload = await cropImageWithBBoxWithTolerance(safeCapturePath, uiCropBbox, 0.06);
|
|
355
299
|
}
|
|
@@ -357,26 +301,18 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
357
301
|
console.warn("Crop failed, falling back to original image", e);
|
|
358
302
|
imagePathForUpload = safeCapturePath;
|
|
359
303
|
}
|
|
360
|
-
const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://'))
|
|
361
|
-
? `file://${imagePathForUpload}`
|
|
362
|
-
: imagePathForUpload;
|
|
304
|
+
const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://')) ? `file://${imagePathForUpload}` : imagePathForUpload;
|
|
363
305
|
const base64 = await pathToBase64(safeCroppedPath);
|
|
306
|
+
const side = currentSideRef.current; // Grab live ref
|
|
364
307
|
const newImages = {
|
|
365
|
-
...
|
|
366
|
-
|
|
367
|
-
// Image source={{ uri }} never receives a bare path on iOS.
|
|
368
|
-
[currentSide]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
308
|
+
...capturedImagesRef.current,
|
|
309
|
+
[side]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
369
310
|
};
|
|
370
311
|
setCapturedImages(newImages);
|
|
371
312
|
if (verified.country && verified.documentType) {
|
|
372
313
|
const docType = verified.documentType;
|
|
373
314
|
const resolvedDocumentType = GovernmentDocumentTypeBackend[docType] || (Object.values(GovernmentDocumentTypeBackend).includes(docType) ? docType : null) || Object.entries(GovernmentDocumentTypeBackend).find(([, v]) => v === docType)?.[1] || docType;
|
|
374
|
-
onValueChange({
|
|
375
|
-
...newImages,
|
|
376
|
-
country: verified.country,
|
|
377
|
-
documentType: resolvedDocumentType,
|
|
378
|
-
...(verified.region ? { region: verified.region } : {})
|
|
379
|
-
});
|
|
315
|
+
onValueChange({ ...newImages, country: verified.country, documentType: resolvedDocumentType, ...(verified.region ? { region: verified.region } : {}) });
|
|
380
316
|
}
|
|
381
317
|
setTimeout(() => {
|
|
382
318
|
setShowCamera(false);
|
|
@@ -389,7 +325,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
389
325
|
}, 600);
|
|
390
326
|
}
|
|
391
327
|
catch (e) {
|
|
392
|
-
console.error("Backend Error:", e);
|
|
393
328
|
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
394
329
|
showAlert('Error', friendlyError);
|
|
395
330
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
@@ -397,47 +332,42 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
397
332
|
setProcessingImagePath(null);
|
|
398
333
|
}
|
|
399
334
|
};
|
|
400
|
-
const isBarcodeRequired = () => {
|
|
401
|
-
if (
|
|
335
|
+
const isBarcodeRequired = (side, cData, docType) => {
|
|
336
|
+
if (side !== 'back')
|
|
402
337
|
return false;
|
|
403
|
-
if (!
|
|
338
|
+
if (!cData?.code || !docType?.type)
|
|
404
339
|
return false;
|
|
405
|
-
const rawDocType =
|
|
406
|
-
const jsonDocMap = {
|
|
407
|
-
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
408
|
-
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
409
|
-
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
410
|
-
};
|
|
340
|
+
const rawDocType = docType.type;
|
|
341
|
+
const jsonDocMap = { 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card' };
|
|
411
342
|
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
412
|
-
const rawCountryName = ISO_TO_COUNTRY_NAME[
|
|
343
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[cData.code] || cData.code;
|
|
413
344
|
const baseMapping = REGION_MAPPING.regionMapping || REGION_MAPPING;
|
|
414
345
|
let countryMapping = baseMapping[rawCountryName];
|
|
415
346
|
if (!countryMapping) {
|
|
416
|
-
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() ===
|
|
347
|
+
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === cData.code.toLowerCase());
|
|
417
348
|
if (foundKey)
|
|
418
349
|
countryMapping = baseMapping[foundKey];
|
|
419
350
|
}
|
|
420
351
|
if (!countryMapping || !countryMapping[searchDocType])
|
|
421
352
|
return false;
|
|
422
|
-
const rawRegionKey =
|
|
353
|
+
const rawRegionKey = cData.region?.trim()?.length > 0 ? cData.region.trim() : 'root';
|
|
423
354
|
const regionKey = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
424
355
|
const regionData = countryMapping[searchDocType][regionKey];
|
|
425
356
|
if (!regionData || !Array.isArray(regionData))
|
|
426
357
|
return false;
|
|
427
|
-
|
|
358
|
+
return regionData.some((item) => {
|
|
428
359
|
const method = String(item['back'] || '').toUpperCase();
|
|
429
360
|
return method.includes('BARCODE') || method.includes('2D');
|
|
430
361
|
});
|
|
431
|
-
logger.log(`[Barcode Check] Dynamic check for ${countryData.code} - ${searchDocType} - ${regionKey}: requiresBarcode=${requiresBarcode}`);
|
|
432
|
-
return requiresBarcode;
|
|
433
362
|
};
|
|
434
363
|
const processCanadianBarcode = async (barcodeString) => {
|
|
435
|
-
if (
|
|
364
|
+
if (isProcessingCaptureRef.current || !pendingCAImage)
|
|
436
365
|
return;
|
|
437
|
-
logger.log('[CA Barcode] Valid barcode decoded. Sending to parser...');
|
|
438
366
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
|
|
439
367
|
try {
|
|
440
|
-
const
|
|
368
|
+
const docType = selectedDocTypeRef.current;
|
|
369
|
+
const cData = countryDataRef.current;
|
|
370
|
+
const isHIC = docType.type === 'health_insurance_card';
|
|
441
371
|
let parsedDictionary;
|
|
442
372
|
if (isHIC && barcodeString.length < 50) {
|
|
443
373
|
parsedDictionary = { data: { card_number: barcodeString, barcode_type: "1D" } };
|
|
@@ -449,16 +379,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
449
379
|
if (!parsedDictionary)
|
|
450
380
|
throw new Error("Could not parse the barcode data.");
|
|
451
381
|
const verifiedResult = {
|
|
452
|
-
path: pendingCAImage.path,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
success: true,
|
|
456
|
-
mrz: JSON.stringify(parsedDictionary),
|
|
457
|
-
barcode: barcodeString,
|
|
458
|
-
isAnalyzing: false,
|
|
459
|
-
country: countryData?.code,
|
|
460
|
-
documentType: selectedDocumentType.type,
|
|
461
|
-
region: selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined,
|
|
382
|
+
path: pendingCAImage.path, templatePath: pendingCAImage.templatePath, bbox: pendingCAImage.bbox, success: true,
|
|
383
|
+
mrz: JSON.stringify(parsedDictionary), barcode: barcodeString, isAnalyzing: false,
|
|
384
|
+
country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined,
|
|
462
385
|
};
|
|
463
386
|
setSilentCaptureResult(verifiedResult);
|
|
464
387
|
if (pendingCAImage.bbox) {
|
|
@@ -468,7 +391,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
468
391
|
await autoCapture(pendingCAImage.path, verifiedResult);
|
|
469
392
|
}
|
|
470
393
|
catch (error) {
|
|
471
|
-
console.error("Barcode processing failed", error);
|
|
472
394
|
setScanPhase('image');
|
|
473
395
|
setPendingCAImage(null);
|
|
474
396
|
scannedBarcodeRef.current = null;
|
|
@@ -482,42 +404,30 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
482
404
|
return;
|
|
483
405
|
if (isAnalyzingRef.current)
|
|
484
406
|
return;
|
|
407
|
+
// USE REFS TO AVOID IOS NATIVE STALE CLOSURE
|
|
408
|
+
const side = currentSideRef.current;
|
|
409
|
+
const captured = capturedImagesRef.current;
|
|
410
|
+
const docType = selectedDocTypeRef.current;
|
|
411
|
+
const cData = countryDataRef.current;
|
|
485
412
|
if (result.success && result.path) {
|
|
486
413
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
487
|
-
const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://')
|
|
488
|
-
|
|
489
|
-
: result.path;
|
|
490
|
-
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
414
|
+
const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
|
|
415
|
+
let templatePath = captured[side]?.templatePath || '';
|
|
491
416
|
let templateBbox;
|
|
492
417
|
let templateResponse;
|
|
493
|
-
if (!
|
|
418
|
+
if (!docType) {
|
|
494
419
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
495
420
|
return;
|
|
496
421
|
}
|
|
497
|
-
const issuingSubdivision =
|
|
498
|
-
// 🚨 WATCHDOG TIMER: If the iOS network drops and doesn't throw, this guarantees the UI unlocks after 20 seconds.
|
|
422
|
+
const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
|
|
499
423
|
const watchdog = setTimeout(() => {
|
|
500
424
|
if (isAnalyzingRef.current) {
|
|
501
|
-
setSilentCaptureResult((prev) => ({
|
|
502
|
-
...prev,
|
|
503
|
-
isAnalyzing: false,
|
|
504
|
-
success: false,
|
|
505
|
-
error: state.currentLanguage === 'en' ? 'Connection timeout. Please try again.' : 'Délai de connexion dépassé. Veuillez réessayer.'
|
|
506
|
-
}));
|
|
507
|
-
Alert.alert(state.currentLanguage === 'en' ? "Timeout" : "Délai dépassé", state.currentLanguage === 'en'
|
|
508
|
-
? "The request took too long. Please verify your network and try again."
|
|
509
|
-
: "La requête a pris trop de temps. Veuillez vérifier votre connexion et réessayer.");
|
|
425
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.' }));
|
|
510
426
|
}
|
|
511
427
|
}, 20000);
|
|
512
428
|
try {
|
|
513
429
|
if (!templatePath) {
|
|
514
|
-
const templateType = await checkTemplateType({
|
|
515
|
-
path: safeCapturePath,
|
|
516
|
-
docType: selectedDocumentType?.type,
|
|
517
|
-
docRegion: countryData?.code || "",
|
|
518
|
-
postfix: currentSide,
|
|
519
|
-
issuingSubdivision: issuingSubdivision
|
|
520
|
-
}, env);
|
|
430
|
+
const templateType = await checkTemplateType({ path: safeCapturePath, docType: docType.type, docRegion: cData?.code || "", postfix: side, issuingSubdivision: issuingSubdivision }, env);
|
|
521
431
|
templateResponse = templateType;
|
|
522
432
|
if (templateType.template_path)
|
|
523
433
|
templatePath = templateType.template_path;
|
|
@@ -535,57 +445,27 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
535
445
|
catch { }
|
|
536
446
|
}
|
|
537
447
|
}
|
|
538
|
-
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[
|
|
539
|
-
const regionMappings = getCurrentSideVerification(
|
|
448
|
+
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[cData?.code || ''] || 'root');
|
|
449
|
+
const regionMappings = getCurrentSideVerification(side, extractedCountryKey, cData);
|
|
540
450
|
let verificationRes;
|
|
541
|
-
if (
|
|
451
|
+
if (side === 'front') {
|
|
542
452
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
543
453
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
544
|
-
verificationRes = await frontVerification({
|
|
545
|
-
path: safeCapturePath,
|
|
546
|
-
regionMapping: {
|
|
547
|
-
authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
|
|
548
|
-
mrzTypes: mrzType ? [mrzType] : []
|
|
549
|
-
},
|
|
550
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
|
|
551
|
-
code: countryData?.code || '',
|
|
552
|
-
currentSide,
|
|
553
|
-
templatePath,
|
|
554
|
-
mrzType,
|
|
555
|
-
issuingSubdivision: issuingSubdivision,
|
|
556
|
-
clientSideBarcodeData: silentCaptureResult.barcode
|
|
557
|
-
}, env);
|
|
454
|
+
verificationRes = await frontVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType, issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode }, env);
|
|
558
455
|
}
|
|
559
456
|
else {
|
|
560
|
-
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
561
457
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
562
458
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
563
|
-
const frontTemplatePath =
|
|
564
|
-
verificationRes = await backVerification({
|
|
565
|
-
path: safeCapturePath,
|
|
566
|
-
regionMapping: {
|
|
567
|
-
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
|
|
568
|
-
mrzTypes: backMrzType ? [backMrzType] : []
|
|
569
|
-
},
|
|
570
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
|
|
571
|
-
code: countryData?.code || '',
|
|
572
|
-
currentSide,
|
|
573
|
-
templatePath,
|
|
574
|
-
mrzType: backMrzType,
|
|
575
|
-
templateResponse,
|
|
576
|
-
issuingSubdivision: issuingSubdivision,
|
|
577
|
-
frontTemplatePath: frontTemplatePath
|
|
578
|
-
}, env);
|
|
459
|
+
const frontTemplatePath = captured['front']?.templatePath || '';
|
|
460
|
+
verificationRes = await backVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType: backMrzType, templateResponse, issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath }, env);
|
|
579
461
|
}
|
|
580
462
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
581
463
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
582
|
-
if (isBarcodeRequired()) {
|
|
464
|
+
if (isBarcodeRequired(side, cData, docType)) {
|
|
583
465
|
const barcodeData = scannedBarcodeRef.current;
|
|
584
|
-
const
|
|
585
|
-
const isHIC = docTypeStr === 'health_insurance_card';
|
|
466
|
+
const isHIC = docType.type === 'health_insurance_card';
|
|
586
467
|
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
587
468
|
if (isValidScan) {
|
|
588
|
-
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
589
469
|
let parsedDictionary;
|
|
590
470
|
if (isHIC && barcodeData.length < 50) {
|
|
591
471
|
parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
|
|
@@ -594,69 +474,50 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
594
474
|
const token = await authentification();
|
|
595
475
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
596
476
|
}
|
|
597
|
-
const verifiedResult = {
|
|
598
|
-
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
599
|
-
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
600
|
-
country: countryData?.code, documentType: selectedDocumentType.type,
|
|
601
|
-
region: selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined,
|
|
602
|
-
};
|
|
477
|
+
const verifiedResult = { path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true, mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined };
|
|
603
478
|
clearTimeout(watchdog);
|
|
604
479
|
setSilentCaptureResult(verifiedResult);
|
|
605
|
-
if (bbox)
|
|
606
|
-
|
|
607
|
-
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
608
|
-
}
|
|
480
|
+
if (bbox)
|
|
481
|
+
setBboxBySide(prev => ({ ...prev, ['back']: bbox }));
|
|
609
482
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
610
483
|
return;
|
|
611
484
|
}
|
|
612
|
-
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
613
485
|
clearTimeout(watchdog);
|
|
614
486
|
setPendingCAImage({ path: safeCapturePath, templateResponse, templatePath, bbox });
|
|
615
487
|
setScanPhase('barcode');
|
|
616
488
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
617
489
|
return;
|
|
618
490
|
}
|
|
619
|
-
const verifiedResult = {
|
|
620
|
-
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
621
|
-
isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type,
|
|
622
|
-
region: issuingSubdivision
|
|
623
|
-
};
|
|
491
|
+
const verifiedResult = { path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: issuingSubdivision };
|
|
624
492
|
clearTimeout(watchdog);
|
|
625
493
|
setSilentCaptureResult(verifiedResult);
|
|
626
494
|
if (bbox)
|
|
627
|
-
setBboxBySide(prev => ({ ...prev, [
|
|
495
|
+
setBboxBySide(prev => ({ ...prev, [side]: bbox }));
|
|
628
496
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
629
497
|
}
|
|
630
498
|
catch (error) {
|
|
631
499
|
clearTimeout(watchdog);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
error.message === 'Network Error' ||
|
|
635
|
-
error.code === 'ECONNABORTED' ||
|
|
636
|
-
error.message?.includes('Network');
|
|
637
|
-
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
500
|
+
const isNetworkError = !error.response || error.message === 'Network Error' || error.code === 'ECONNABORTED' || error.message?.includes('Network');
|
|
501
|
+
let hintMessage = '';
|
|
638
502
|
if (isNetworkError) {
|
|
639
|
-
|
|
640
|
-
? 'Cannot connect to verification server. Please check your internet connection.'
|
|
641
|
-
: 'Impossible de se connecter au serveur de vérification. Veuillez vérifier votre connexion internet.';
|
|
642
|
-
Alert.alert(state.currentLanguage === 'en' ? "Connection Failed" : "Échec de connexion", userFriendlyMessage);
|
|
503
|
+
hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Retrying...' : 'Connexion faible. Nouvelle tentative...';
|
|
643
504
|
}
|
|
644
505
|
else {
|
|
645
506
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
646
507
|
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
647
|
-
|
|
508
|
+
hintMessage = state.currentLanguage === 'en' ? 'Move to better lighting.' : 'Améliorez l\'éclairage.';
|
|
648
509
|
}
|
|
649
510
|
else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
650
|
-
|
|
511
|
+
hintMessage = state.currentLanguage === 'en' ? 'Wrong document type.' : 'Mauvais type de document.';
|
|
651
512
|
}
|
|
652
513
|
else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
653
|
-
|
|
514
|
+
hintMessage = state.currentLanguage === 'en' ? 'Move closer.' : 'Rapprochez-vous.';
|
|
654
515
|
}
|
|
655
516
|
else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
656
|
-
|
|
517
|
+
hintMessage = state.currentLanguage === 'en' ? 'Scan the same ID.' : 'Scannez la même pièce.';
|
|
657
518
|
}
|
|
658
519
|
}
|
|
659
|
-
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error:
|
|
520
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: hintMessage }));
|
|
660
521
|
}
|
|
661
522
|
}
|
|
662
523
|
};
|
|
@@ -664,84 +525,33 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
664
525
|
if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
|
|
665
526
|
const bData = silentCaptureResult.barcode;
|
|
666
527
|
if (typeof bData === 'string' && bData.length > 50) {
|
|
667
|
-
logger.log('[CA Barcode] Safety Net Triggered: Processing barcode.');
|
|
668
528
|
processCanadianBarcode(bData);
|
|
669
529
|
}
|
|
670
530
|
}
|
|
671
531
|
}, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
|
|
672
532
|
const handleError = (event) => {
|
|
673
|
-
logger.error("Camera Error:", event);
|
|
674
|
-
// Normalize the error to lowercase for easy checking
|
|
675
533
|
const errString = `${event.message} ${event.name} ${event.code}`.toLowerCase();
|
|
676
|
-
let friendlyMessage = state.currentLanguage === 'en'
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// 1. Detect Hardware Lock (Camera in use by Zoom, Teams, or another app)
|
|
680
|
-
if (errString.includes('notreadableerror') ||
|
|
681
|
-
errString.includes('trackstarterror') ||
|
|
682
|
-
errString.includes('in use') ||
|
|
683
|
-
errString.includes('concurrent') ||
|
|
684
|
-
errString.includes('camera unavailable')) {
|
|
685
|
-
friendlyMessage = state.currentLanguage === 'en'
|
|
686
|
-
? 'Your camera is currently being used by another application (like Zoom or Teams). Please close the other app and try again.'
|
|
687
|
-
: 'Votre caméra est actuellement utilisée par une autre application. Veuillez fermer l\'autre application et réessayer.';
|
|
534
|
+
let friendlyMessage = state.currentLanguage === 'en' ? 'Camera error.' : 'Erreur de caméra.';
|
|
535
|
+
if (errString.includes('notreadableerror') || errString.includes('trackstarterror') || errString.includes('in use') || errString.includes('concurrent') || errString.includes('camera unavailable')) {
|
|
536
|
+
friendlyMessage = state.currentLanguage === 'en' ? 'Camera is in use by another app.' : 'Caméra utilisée par une autre application.';
|
|
688
537
|
}
|
|
689
|
-
// 2. Detect Permission Denied
|
|
690
538
|
else if (errString.includes('notallowederror') || errString.includes('permission')) {
|
|
691
|
-
friendlyMessage = state.currentLanguage === 'en'
|
|
692
|
-
? 'Camera permission denied. Please allow camera access in your settings to proceed.'
|
|
693
|
-
: 'Autorisation de la caméra refusée. Veuillez autoriser l\'accès dans vos paramètres.';
|
|
539
|
+
friendlyMessage = state.currentLanguage === 'en' ? 'Allow camera access in settings.' : 'Autorisez l\'accès à la caméra.';
|
|
694
540
|
}
|
|
695
|
-
// 3. Detect No Camera Found
|
|
696
541
|
else if (errString.includes('notfounderror') || errString.includes('no camera')) {
|
|
697
|
-
friendlyMessage = state.currentLanguage === 'en'
|
|
698
|
-
? 'No camera detected on this device.'
|
|
699
|
-
: 'Aucune caméra détectée sur cet appareil.';
|
|
542
|
+
friendlyMessage = state.currentLanguage === 'en' ? 'No camera detected.' : 'Aucune caméra détectée.';
|
|
700
543
|
}
|
|
701
|
-
showAlert(state.currentLanguage === 'en' ? 'Camera Error' : 'Erreur
|
|
544
|
+
showAlert(state.currentLanguage === 'en' ? 'Camera Error' : 'Erreur', friendlyMessage);
|
|
702
545
|
setShowCamera(false);
|
|
703
546
|
setIsProcessingCapture(false);
|
|
704
547
|
};
|
|
705
|
-
useEffect(() => {
|
|
706
|
-
|
|
707
|
-
}, [showCamera]);
|
|
708
|
-
useEffect(() => {
|
|
709
|
-
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
710
|
-
try {
|
|
711
|
-
const url = new URL(window.location.origin);
|
|
712
|
-
if (url.hostname === 'localhost') {
|
|
713
|
-
url.hostname = '192.168.0.33';
|
|
714
|
-
}
|
|
715
|
-
const params = new URLSearchParams(window.location.search);
|
|
716
|
-
params.set('session_id', state.session.session_id);
|
|
717
|
-
params.set('resume', 'true');
|
|
718
|
-
if (state.currentComponentIndex !== undefined) {
|
|
719
|
-
params.set('component_index', String(state.currentComponentIndex));
|
|
720
|
-
}
|
|
721
|
-
if (countryData?.code) {
|
|
722
|
-
params.set('country', countryData.code);
|
|
723
|
-
}
|
|
724
|
-
if (selectedDocumentType?.type) {
|
|
725
|
-
params.set('document_type', selectedDocumentType.type);
|
|
726
|
-
}
|
|
727
|
-
if (selectedDocumentType?.region && selectedDocumentType.region !== 'root') {
|
|
728
|
-
params.set('region', selectedDocumentType.region);
|
|
729
|
-
}
|
|
730
|
-
url.hash = params.toString();
|
|
731
|
-
setCurrentUrl(url.toString());
|
|
732
|
-
}
|
|
733
|
-
catch (e) {
|
|
734
|
-
console.error("Failed to construct QR handoff URL", e);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}, [state.session.session_id, state.currentComponentIndex, countryData, selectedDocumentType]);
|
|
548
|
+
useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
|
|
549
|
+
// Render logic remains identical below...
|
|
738
550
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
739
551
|
return (<View style={styles.root}>
|
|
740
552
|
<View style={styles.previewContainer}>
|
|
741
553
|
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
742
|
-
<Text style={styles.description}>
|
|
743
|
-
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
744
|
-
</Text>
|
|
554
|
+
<Text style={styles.description}>{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}</Text>
|
|
745
555
|
</View>
|
|
746
556
|
</View>);
|
|
747
557
|
}
|
|
@@ -756,23 +566,15 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
756
566
|
return (<View style={styles.root}>
|
|
757
567
|
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
758
568
|
<View style={styles.headerContainer}>
|
|
759
|
-
<Text style={styles.headerTitle}>
|
|
760
|
-
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
761
|
-
</Text>
|
|
569
|
+
<Text style={styles.headerTitle}>{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}</Text>
|
|
762
570
|
<View style={styles.stepBadge}>
|
|
763
|
-
<Text style={styles.stepText}>
|
|
764
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
765
|
-
</Text>
|
|
571
|
+
<Text style={styles.stepText}>{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}</Text>
|
|
766
572
|
</View>
|
|
767
573
|
</View>
|
|
768
|
-
|
|
769
574
|
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
770
575
|
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
771
|
-
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
772
|
-
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
773
|
-
</TouchableOpacity>
|
|
576
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}><Text style={styles.webFlipText}>🔄 Flip Lens</Text></TouchableOpacity>
|
|
774
577
|
</View>)}
|
|
775
|
-
|
|
776
578
|
<EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} isScanningBarcode={currentSide === 'back'} barcodeTypes={targetBarcodeTypes} onBarcodeScanned={(barcodeString) => {
|
|
777
579
|
const docTypeStr = selectedDocumentType?.type;
|
|
778
580
|
const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
|
|
@@ -787,8 +589,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
787
589
|
setSilentCaptureResult(prev => ({ ...prev, barcode: barcodeString }));
|
|
788
590
|
}
|
|
789
591
|
}
|
|
790
|
-
}} overlayComponent={
|
|
791
|
-
<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success || scanPhase === 'barcode'} language={state.currentLanguage} stepperProps={{
|
|
592
|
+
}} overlayComponent={<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success || scanPhase === 'barcode'} language={state.currentLanguage} stepperProps={{
|
|
792
593
|
back: () => {
|
|
793
594
|
if (currentSide === 'back') {
|
|
794
595
|
scannedBarcodeRef.current = null;
|
|
@@ -800,9 +601,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
800
601
|
setProcessingImagePath(null);
|
|
801
602
|
if (capturedImages['front']?.dir) {
|
|
802
603
|
const frontImage = capturedImages['front'];
|
|
803
|
-
setSilentCaptureResult((prev) => ({
|
|
804
|
-
...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front']
|
|
805
|
-
}));
|
|
604
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
806
605
|
}
|
|
807
606
|
else {
|
|
808
607
|
setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
|
|
@@ -812,72 +611,41 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
812
611
|
actions.previousComponent();
|
|
813
612
|
}
|
|
814
613
|
},
|
|
815
|
-
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
816
|
-
|
|
817
|
-
totalSteps: state.template.components.length,
|
|
818
|
-
side: currentSide,
|
|
819
|
-
}}/>
|
|
820
|
-
</>}/>
|
|
821
|
-
|
|
614
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '', step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
615
|
+
}}/>}/>
|
|
822
616
|
{scanPhase === 'barcode' && !isBusy && (<View style={styles.barcodePromptContainer}>
|
|
823
617
|
<View style={styles.barcodePromptContent}>
|
|
824
|
-
<View style={styles.barcodeIconContainer}>
|
|
825
|
-
<Text style={styles.barcodeIcon}>✓</Text>
|
|
826
|
-
</View>
|
|
618
|
+
<View style={styles.barcodeIconContainer}><Text style={styles.barcodeIcon}>✓</Text></View>
|
|
827
619
|
<View style={styles.barcodeTextContainer}>
|
|
828
|
-
<Text style={styles.barcodePromptTitle}>
|
|
829
|
-
|
|
830
|
-
</Text>
|
|
831
|
-
<Text style={styles.barcodePromptDesc}>
|
|
832
|
-
{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}
|
|
833
|
-
</Text>
|
|
620
|
+
<Text style={styles.barcodePromptTitle}>{state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}</Text>
|
|
621
|
+
<Text style={styles.barcodePromptDesc}>{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}</Text>
|
|
834
622
|
</View>
|
|
835
623
|
</View>
|
|
836
624
|
</View>)}
|
|
837
|
-
|
|
838
625
|
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
839
626
|
<View style={styles.topAnalyzingPill}>
|
|
840
627
|
<ActivityIndicator size="small" color="white"/>
|
|
841
|
-
<Text style={styles.analyzingPillText}>
|
|
842
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
843
|
-
</Text>
|
|
628
|
+
<Text style={styles.analyzingPillText}>{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}</Text>
|
|
844
629
|
</View>
|
|
845
630
|
</View>)}
|
|
846
|
-
|
|
847
631
|
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
848
|
-
{processingImagePath &&
|
|
632
|
+
{processingImagePath && <Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>}
|
|
849
633
|
<View style={styles.processingOverlay}>
|
|
850
634
|
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
851
|
-
<Text style={styles.processingText}>
|
|
852
|
-
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
853
|
-
</Text>
|
|
635
|
+
<Text style={styles.processingText}>{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}</Text>
|
|
854
636
|
</View>
|
|
855
637
|
</View>)}
|
|
856
|
-
|
|
857
638
|
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
858
|
-
{silentCaptureResult.error &&
|
|
859
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
860
|
-
</View>)}
|
|
639
|
+
{silentCaptureResult.error && <View style={styles.floatingErrorBanner}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>}
|
|
861
640
|
<View style={styles.webActionButtonsRow}>
|
|
862
|
-
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
863
|
-
|
|
864
|
-
</TouchableOpacity>
|
|
865
|
-
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
866
|
-
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
867
|
-
</TouchableOpacity>
|
|
641
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
|
|
642
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}><Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text></TouchableOpacity>
|
|
868
643
|
</View>
|
|
869
644
|
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
870
|
-
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
871
|
-
|
|
872
|
-
</TouchableOpacity>
|
|
873
|
-
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
874
|
-
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
875
|
-
</TouchableOpacity>
|
|
876
|
-
</View>) : null}
|
|
877
|
-
|
|
878
|
-
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
879
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
645
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}><Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text></TouchableOpacity>
|
|
646
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}><Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text></TouchableOpacity>
|
|
880
647
|
</View>) : null}
|
|
648
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>) : null}
|
|
881
649
|
</View>
|
|
882
650
|
</View>
|
|
883
651
|
</View>);
|
|
@@ -886,47 +654,24 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
886
654
|
<View style={styles.previewContainer}>
|
|
887
655
|
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
888
656
|
<View key={currentSide} style={styles.sideContainer}>
|
|
889
|
-
<Text style={styles.sideTitle}>
|
|
890
|
-
|
|
891
|
-
</Text>
|
|
892
|
-
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
893
|
-
{getLocalizedText(component.instructions)}
|
|
894
|
-
</Text>
|
|
895
|
-
|
|
657
|
+
<Text style={styles.sideTitle}>{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}</Text>
|
|
658
|
+
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>{getLocalizedText(component.instructions)}</Text>
|
|
896
659
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
897
660
|
<View style={styles.imagePreviewWrapper}>
|
|
898
661
|
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
899
662
|
</View>
|
|
900
|
-
|
|
901
663
|
{!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
|
|
902
|
-
|
|
903
664
|
{isWebScanningRestricted ? (<>
|
|
904
665
|
<View style={styles.restrictionWarningBanner}>
|
|
905
|
-
<Text style={styles.restrictionWarningTitle}>
|
|
906
|
-
|
|
907
|
-
</Text>
|
|
908
|
-
<Text style={styles.restrictionWarningText}>
|
|
909
|
-
{state.currentLanguage === 'en'
|
|
910
|
-
? `This document can only be scanned using our mobile app.`
|
|
911
|
-
: `Ce document ne peut être scanné qu'avec notre application mobile.`}
|
|
912
|
-
</Text>
|
|
666
|
+
<Text style={styles.restrictionWarningTitle}>{state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}</Text>
|
|
667
|
+
<Text style={styles.restrictionWarningText}>{state.currentLanguage === 'en' ? `This document can only be scanned using our mobile app.` : `Ce document ne peut être scanné qu'avec notre application mobile.`}</Text>
|
|
913
668
|
</View>
|
|
914
|
-
|
|
915
669
|
<Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="primary" size="large" fullWidth/>
|
|
916
|
-
</>) : (
|
|
917
|
-
|
|
918
|
-
<>
|
|
919
|
-
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => {
|
|
920
|
-
setShowCamera(true);
|
|
921
|
-
refreshCamera();
|
|
922
|
-
actions.showCustomStepper(false);
|
|
923
|
-
}} variant="primary" size="large" fullWidth/>
|
|
924
|
-
|
|
670
|
+
</>) : (<>
|
|
671
|
+
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
|
|
925
672
|
{Platform.OS === 'web' && (<Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="outline" size="large" fullWidth/>)}
|
|
926
673
|
</>)}
|
|
927
|
-
|
|
928
674
|
</View>)}
|
|
929
|
-
|
|
930
675
|
{capturedImages[currentSide]?.dir && (<>
|
|
931
676
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
932
677
|
<Button title={t('common.next')} onPress={() => {
|
|
@@ -953,68 +698,68 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
953
698
|
</View>
|
|
954
699
|
</ScrollView>
|
|
955
700
|
</View>
|
|
956
|
-
|
|
701
|
+
|
|
957
702
|
{showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
703
|
+
<View style={styles.qrModalContainer}>
|
|
704
|
+
<View style={styles.qrModalHeader}>
|
|
705
|
+
<Text style={styles.qrModalTitle}>
|
|
706
|
+
{state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
|
|
707
|
+
</Text>
|
|
708
|
+
<Text style={styles.qrModalText}>
|
|
709
|
+
{state.currentLanguage === 'en'
|
|
965
710
|
? "Scan the QR code to download our mobile app and complete the scanning process securely."
|
|
966
711
|
: "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
|
|
967
|
-
|
|
968
|
-
|
|
712
|
+
</Text>
|
|
713
|
+
</View>
|
|
969
714
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
715
|
+
<View style={styles.qrToggleContainer}>
|
|
716
|
+
<TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('android')}>
|
|
717
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
|
|
718
|
+
Android
|
|
719
|
+
</Text>
|
|
720
|
+
</TouchableOpacity>
|
|
721
|
+
<TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('ios')}>
|
|
722
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
|
|
723
|
+
iOS (Apple)
|
|
724
|
+
</Text>
|
|
725
|
+
</TouchableOpacity>
|
|
726
|
+
</View>
|
|
982
727
|
|
|
983
|
-
|
|
984
|
-
|
|
728
|
+
<View style={styles.qrCodeWrapper}>
|
|
729
|
+
<QRCode value={qrPlatform === 'android'
|
|
985
730
|
? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
|
|
986
731
|
: 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'} size={180} backgroundColor="#FFFFFF" color="#0F172A"/>
|
|
987
|
-
|
|
732
|
+
</View>
|
|
988
733
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
734
|
+
<View style={styles.linkCodeSection}>
|
|
735
|
+
<Text style={styles.linkCodeHeader}>
|
|
736
|
+
{state.currentLanguage === 'en' ? 'Use code below to proceed on the Sanctum Key App.' : 'Vous avez déjà l\'application ? Liez-la :'}
|
|
737
|
+
</Text>
|
|
738
|
+
|
|
739
|
+
{generatedCode ? (<ExpiringCodeTimer initialCode={generatedCode} language={state.currentLanguage}/>) : (<TouchableOpacity style={styles.generateCodeButton} onPress={handleGenerateCode} disabled={isGeneratingCode}>
|
|
740
|
+
{isGeneratingCode ? (<ActivityIndicator size="small" color="#0F172A"/>) : (<Text style={styles.generateCodeButtonText}>
|
|
741
|
+
{state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
|
|
742
|
+
</Text>)}
|
|
743
|
+
</TouchableOpacity>)}
|
|
744
|
+
</View>
|
|
1000
745
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
746
|
+
{isWebScanningRestricted && (<View style={styles.storeLinksContainer}>
|
|
747
|
+
<Text style={styles.storeLinksTitle}>
|
|
748
|
+
{state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
|
|
749
|
+
</Text>
|
|
750
|
+
<View style={styles.storeButtonsRow}>
|
|
751
|
+
<TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')}>
|
|
752
|
+
<Text style={styles.storeButtonText}>Google Play</Text>
|
|
753
|
+
</TouchableOpacity>
|
|
754
|
+
<TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')}>
|
|
755
|
+
<Text style={styles.storeButtonText}>App Store</Text>
|
|
756
|
+
</TouchableOpacity>
|
|
757
|
+
</View>
|
|
758
|
+
</View>)}
|
|
1014
759
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
760
|
+
<Button title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
|
|
761
|
+
</View>
|
|
762
|
+
</View>)}
|
|
1018
763
|
</View>);
|
|
1019
764
|
};
|
|
1020
765
|
const styles = StyleSheet.create({
|