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