@sanctum-key/react-native-sdk 1.0.37 → 1.0.39
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 +190 -412
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +306 -688
|
@@ -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(() => {
|
|
@@ -66,16 +63,52 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
66
63
|
const [cameraType, setCameraType] = useState('back');
|
|
67
64
|
const [showQRModal, setShowQRModal] = useState(false);
|
|
68
65
|
const [currentUrl, setCurrentUrl] = useState('');
|
|
66
|
+
console.log(currentUrl);
|
|
69
67
|
const [qrPlatform, setQrPlatform] = useState('android');
|
|
70
68
|
const [scanPhase, setScanPhase] = useState('image');
|
|
71
69
|
const [pendingCAImage, setPendingCAImage] = useState(null);
|
|
72
70
|
const scannedBarcodeRef = useRef(null);
|
|
73
|
-
|
|
71
|
+
const [generatedCode, setGeneratedCode] = useState(null);
|
|
72
|
+
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
|
73
|
+
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
74
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
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,37 +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
414
|
const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
|
|
488
|
-
let templatePath =
|
|
415
|
+
let templatePath = captured[side]?.templatePath || '';
|
|
489
416
|
let templateBbox;
|
|
490
417
|
let templateResponse;
|
|
491
|
-
if (!
|
|
418
|
+
if (!docType) {
|
|
492
419
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
493
420
|
return;
|
|
494
421
|
}
|
|
495
|
-
const issuingSubdivision =
|
|
496
|
-
// Subtle timeout handling without blocking pop-ups
|
|
422
|
+
const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
|
|
497
423
|
const watchdog = setTimeout(() => {
|
|
498
424
|
if (isAnalyzingRef.current) {
|
|
499
|
-
setSilentCaptureResult((prev) => ({
|
|
500
|
-
...prev,
|
|
501
|
-
isAnalyzing: false,
|
|
502
|
-
success: false,
|
|
503
|
-
error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.'
|
|
504
|
-
}));
|
|
425
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.' }));
|
|
505
426
|
}
|
|
506
427
|
}, 20000);
|
|
507
428
|
try {
|
|
508
429
|
if (!templatePath) {
|
|
509
|
-
const templateType = await checkTemplateType({
|
|
510
|
-
path: safeCapturePath,
|
|
511
|
-
docType: selectedDocumentType?.type,
|
|
512
|
-
docRegion: countryData?.code || "",
|
|
513
|
-
postfix: currentSide,
|
|
514
|
-
issuingSubdivision: issuingSubdivision
|
|
515
|
-
}, env);
|
|
430
|
+
const templateType = await checkTemplateType({ path: safeCapturePath, docType: docType.type, docRegion: cData?.code || "", postfix: side, issuingSubdivision: issuingSubdivision }, env);
|
|
516
431
|
templateResponse = templateType;
|
|
517
432
|
if (templateType.template_path)
|
|
518
433
|
templatePath = templateType.template_path;
|
|
@@ -530,51 +445,27 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
530
445
|
catch { }
|
|
531
446
|
}
|
|
532
447
|
}
|
|
533
|
-
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[
|
|
534
|
-
const regionMappings = getCurrentSideVerification(
|
|
448
|
+
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[cData?.code || ''] || 'root');
|
|
449
|
+
const regionMappings = getCurrentSideVerification(side, extractedCountryKey, cData);
|
|
535
450
|
let verificationRes;
|
|
536
|
-
if (
|
|
451
|
+
if (side === 'front') {
|
|
537
452
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
538
453
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
539
|
-
verificationRes = await frontVerification({
|
|
540
|
-
path: safeCapturePath,
|
|
541
|
-
regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
|
|
542
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
|
|
543
|
-
code: countryData?.code || '',
|
|
544
|
-
currentSide,
|
|
545
|
-
templatePath,
|
|
546
|
-
mrzType,
|
|
547
|
-
issuingSubdivision: issuingSubdivision,
|
|
548
|
-
clientSideBarcodeData: silentCaptureResult.barcode
|
|
549
|
-
}, 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);
|
|
550
455
|
}
|
|
551
456
|
else {
|
|
552
|
-
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
553
457
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
554
458
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
555
|
-
const frontTemplatePath =
|
|
556
|
-
verificationRes = await backVerification({
|
|
557
|
-
path: safeCapturePath,
|
|
558
|
-
regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
|
|
559
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
|
|
560
|
-
code: countryData?.code || '',
|
|
561
|
-
currentSide,
|
|
562
|
-
templatePath,
|
|
563
|
-
mrzType: backMrzType,
|
|
564
|
-
templateResponse,
|
|
565
|
-
issuingSubdivision: issuingSubdivision,
|
|
566
|
-
frontTemplatePath: frontTemplatePath
|
|
567
|
-
}, 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);
|
|
568
461
|
}
|
|
569
462
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
570
463
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
571
|
-
if (isBarcodeRequired()) {
|
|
464
|
+
if (isBarcodeRequired(side, cData, docType)) {
|
|
572
465
|
const barcodeData = scannedBarcodeRef.current;
|
|
573
|
-
const
|
|
574
|
-
const isHIC = docTypeStr === 'health_insurance_card';
|
|
466
|
+
const isHIC = docType.type === 'health_insurance_card';
|
|
575
467
|
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
576
468
|
if (isValidScan) {
|
|
577
|
-
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
578
469
|
let parsedDictionary;
|
|
579
470
|
if (isHIC && barcodeData.length < 50) {
|
|
580
471
|
parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
|
|
@@ -583,43 +474,30 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
583
474
|
const token = await authentification();
|
|
584
475
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
585
476
|
}
|
|
586
|
-
const verifiedResult = {
|
|
587
|
-
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
588
|
-
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
589
|
-
country: countryData?.code, documentType: selectedDocumentType.type,
|
|
590
|
-
region: selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined,
|
|
591
|
-
};
|
|
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 };
|
|
592
478
|
clearTimeout(watchdog);
|
|
593
479
|
setSilentCaptureResult(verifiedResult);
|
|
594
|
-
if (bbox)
|
|
595
|
-
|
|
596
|
-
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
597
|
-
}
|
|
480
|
+
if (bbox)
|
|
481
|
+
setBboxBySide(prev => ({ ...prev, ['back']: bbox }));
|
|
598
482
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
599
483
|
return;
|
|
600
484
|
}
|
|
601
|
-
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
602
485
|
clearTimeout(watchdog);
|
|
603
486
|
setPendingCAImage({ path: safeCapturePath, templateResponse, templatePath, bbox });
|
|
604
487
|
setScanPhase('barcode');
|
|
605
488
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
606
489
|
return;
|
|
607
490
|
}
|
|
608
|
-
const verifiedResult = {
|
|
609
|
-
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
610
|
-
isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type,
|
|
611
|
-
region: issuingSubdivision
|
|
612
|
-
};
|
|
491
|
+
const verifiedResult = { path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: issuingSubdivision };
|
|
613
492
|
clearTimeout(watchdog);
|
|
614
493
|
setSilentCaptureResult(verifiedResult);
|
|
615
494
|
if (bbox)
|
|
616
|
-
setBboxBySide(prev => ({ ...prev, [
|
|
495
|
+
setBboxBySide(prev => ({ ...prev, [side]: bbox }));
|
|
617
496
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
618
497
|
}
|
|
619
498
|
catch (error) {
|
|
620
499
|
clearTimeout(watchdog);
|
|
621
500
|
const isNetworkError = !error.response || error.message === 'Network Error' || error.code === 'ECONNABORTED' || error.message?.includes('Network');
|
|
622
|
-
// Keep UI errors extremely brief and actionable
|
|
623
501
|
let hintMessage = '';
|
|
624
502
|
if (isNetworkError) {
|
|
625
503
|
hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Retrying...' : 'Connexion faible. Nouvelle tentative...';
|
|
@@ -647,13 +525,11 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
647
525
|
if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
|
|
648
526
|
const bData = silentCaptureResult.barcode;
|
|
649
527
|
if (typeof bData === 'string' && bData.length > 50) {
|
|
650
|
-
logger.log('[CA Barcode] Safety Net Triggered: Processing barcode.');
|
|
651
528
|
processCanadianBarcode(bData);
|
|
652
529
|
}
|
|
653
530
|
}
|
|
654
531
|
}, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
|
|
655
532
|
const handleError = (event) => {
|
|
656
|
-
logger.error("Camera Error:", event);
|
|
657
533
|
const errString = `${event.message} ${event.name} ${event.code}`.toLowerCase();
|
|
658
534
|
let friendlyMessage = state.currentLanguage === 'en' ? 'Camera error.' : 'Erreur de caméra.';
|
|
659
535
|
if (errString.includes('notreadableerror') || errString.includes('trackstarterror') || errString.includes('in use') || errString.includes('concurrent') || errString.includes('camera unavailable')) {
|
|
@@ -669,46 +545,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
669
545
|
setShowCamera(false);
|
|
670
546
|
setIsProcessingCapture(false);
|
|
671
547
|
};
|
|
672
|
-
useEffect(() => {
|
|
673
|
-
|
|
674
|
-
}, [showCamera]);
|
|
675
|
-
useEffect(() => {
|
|
676
|
-
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
677
|
-
try {
|
|
678
|
-
const url = new URL(window.location.origin);
|
|
679
|
-
if (url.hostname === 'localhost') {
|
|
680
|
-
url.hostname = '192.168.0.33';
|
|
681
|
-
}
|
|
682
|
-
const params = new URLSearchParams(window.location.search);
|
|
683
|
-
params.set('session_id', state.session.session_id);
|
|
684
|
-
params.set('resume', 'true');
|
|
685
|
-
if (state.currentComponentIndex !== undefined) {
|
|
686
|
-
params.set('component_index', String(state.currentComponentIndex));
|
|
687
|
-
}
|
|
688
|
-
if (countryData?.code) {
|
|
689
|
-
params.set('country', countryData.code);
|
|
690
|
-
}
|
|
691
|
-
if (selectedDocumentType?.type) {
|
|
692
|
-
params.set('document_type', selectedDocumentType.type);
|
|
693
|
-
}
|
|
694
|
-
if (selectedDocumentType?.region && selectedDocumentType.region !== 'root') {
|
|
695
|
-
params.set('region', selectedDocumentType.region);
|
|
696
|
-
}
|
|
697
|
-
url.hash = params.toString();
|
|
698
|
-
setCurrentUrl(url.toString());
|
|
699
|
-
}
|
|
700
|
-
catch (e) {
|
|
701
|
-
console.error("Failed to construct QR handoff URL", e);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}, [state.session.session_id, state.currentComponentIndex, countryData, selectedDocumentType]);
|
|
548
|
+
useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
|
|
549
|
+
// Render logic remains identical below...
|
|
705
550
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
706
551
|
return (<View style={styles.root}>
|
|
707
552
|
<View style={styles.previewContainer}>
|
|
708
553
|
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
709
|
-
<Text style={styles.description}>
|
|
710
|
-
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
711
|
-
</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>
|
|
712
555
|
</View>
|
|
713
556
|
</View>);
|
|
714
557
|
}
|
|
@@ -723,23 +566,15 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
723
566
|
return (<View style={styles.root}>
|
|
724
567
|
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
725
568
|
<View style={styles.headerContainer}>
|
|
726
|
-
<Text style={styles.headerTitle}>
|
|
727
|
-
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
728
|
-
</Text>
|
|
569
|
+
<Text style={styles.headerTitle}>{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}</Text>
|
|
729
570
|
<View style={styles.stepBadge}>
|
|
730
|
-
<Text style={styles.stepText}>
|
|
731
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
732
|
-
</Text>
|
|
571
|
+
<Text style={styles.stepText}>{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}</Text>
|
|
733
572
|
</View>
|
|
734
573
|
</View>
|
|
735
|
-
|
|
736
574
|
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
737
575
|
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
738
|
-
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
739
|
-
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
740
|
-
</TouchableOpacity>
|
|
576
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}><Text style={styles.webFlipText}>🔄 Flip Lens</Text></TouchableOpacity>
|
|
741
577
|
</View>)}
|
|
742
|
-
|
|
743
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) => {
|
|
744
579
|
const docTypeStr = selectedDocumentType?.type;
|
|
745
580
|
const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
|
|
@@ -754,8 +589,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
754
589
|
setSilentCaptureResult(prev => ({ ...prev, barcode: barcodeString }));
|
|
755
590
|
}
|
|
756
591
|
}
|
|
757
|
-
}} overlayComponent={
|
|
758
|
-
<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={{
|
|
759
593
|
back: () => {
|
|
760
594
|
if (currentSide === 'back') {
|
|
761
595
|
scannedBarcodeRef.current = null;
|
|
@@ -767,9 +601,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
767
601
|
setProcessingImagePath(null);
|
|
768
602
|
if (capturedImages['front']?.dir) {
|
|
769
603
|
const frontImage = capturedImages['front'];
|
|
770
|
-
setSilentCaptureResult((prev) => ({
|
|
771
|
-
...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front']
|
|
772
|
-
}));
|
|
604
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
773
605
|
}
|
|
774
606
|
else {
|
|
775
607
|
setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
|
|
@@ -779,72 +611,41 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
779
611
|
actions.previousComponent();
|
|
780
612
|
}
|
|
781
613
|
},
|
|
782
|
-
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
783
|
-
|
|
784
|
-
totalSteps: state.template.components.length,
|
|
785
|
-
side: currentSide,
|
|
786
|
-
}}/>
|
|
787
|
-
</>}/>
|
|
788
|
-
|
|
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
|
+
}}/>}/>
|
|
789
616
|
{scanPhase === 'barcode' && !isBusy && (<View style={styles.barcodePromptContainer}>
|
|
790
617
|
<View style={styles.barcodePromptContent}>
|
|
791
|
-
<View style={styles.barcodeIconContainer}>
|
|
792
|
-
<Text style={styles.barcodeIcon}>✓</Text>
|
|
793
|
-
</View>
|
|
618
|
+
<View style={styles.barcodeIconContainer}><Text style={styles.barcodeIcon}>✓</Text></View>
|
|
794
619
|
<View style={styles.barcodeTextContainer}>
|
|
795
|
-
<Text style={styles.barcodePromptTitle}>
|
|
796
|
-
|
|
797
|
-
</Text>
|
|
798
|
-
<Text style={styles.barcodePromptDesc}>
|
|
799
|
-
{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}
|
|
800
|
-
</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>
|
|
801
622
|
</View>
|
|
802
623
|
</View>
|
|
803
624
|
</View>)}
|
|
804
|
-
|
|
805
625
|
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
806
626
|
<View style={styles.topAnalyzingPill}>
|
|
807
627
|
<ActivityIndicator size="small" color="white"/>
|
|
808
|
-
<Text style={styles.analyzingPillText}>
|
|
809
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
810
|
-
</Text>
|
|
628
|
+
<Text style={styles.analyzingPillText}>{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}</Text>
|
|
811
629
|
</View>
|
|
812
630
|
</View>)}
|
|
813
|
-
|
|
814
631
|
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
815
|
-
{processingImagePath &&
|
|
632
|
+
{processingImagePath && <Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>}
|
|
816
633
|
<View style={styles.processingOverlay}>
|
|
817
634
|
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
818
|
-
<Text style={styles.processingText}>
|
|
819
|
-
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
820
|
-
</Text>
|
|
635
|
+
<Text style={styles.processingText}>{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}</Text>
|
|
821
636
|
</View>
|
|
822
637
|
</View>)}
|
|
823
|
-
|
|
824
638
|
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
825
|
-
{silentCaptureResult.error &&
|
|
826
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
827
|
-
</View>)}
|
|
639
|
+
{silentCaptureResult.error && <View style={styles.floatingErrorBanner}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>}
|
|
828
640
|
<View style={styles.webActionButtonsRow}>
|
|
829
|
-
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
830
|
-
|
|
831
|
-
</TouchableOpacity>
|
|
832
|
-
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
833
|
-
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
834
|
-
</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>
|
|
835
643
|
</View>
|
|
836
644
|
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
837
|
-
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
838
|
-
|
|
839
|
-
</TouchableOpacity>
|
|
840
|
-
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
841
|
-
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
842
|
-
</TouchableOpacity>
|
|
843
|
-
</View>) : null}
|
|
844
|
-
|
|
845
|
-
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
846
|
-
<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>
|
|
847
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}
|
|
848
649
|
</View>
|
|
849
650
|
</View>
|
|
850
651
|
</View>);
|
|
@@ -853,47 +654,24 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
853
654
|
<View style={styles.previewContainer}>
|
|
854
655
|
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
855
656
|
<View key={currentSide} style={styles.sideContainer}>
|
|
856
|
-
<Text style={styles.sideTitle}>
|
|
857
|
-
|
|
858
|
-
</Text>
|
|
859
|
-
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
860
|
-
{getLocalizedText(component.instructions)}
|
|
861
|
-
</Text>
|
|
862
|
-
|
|
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>
|
|
863
659
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
864
660
|
<View style={styles.imagePreviewWrapper}>
|
|
865
661
|
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
866
662
|
</View>
|
|
867
|
-
|
|
868
663
|
{!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
|
|
869
|
-
|
|
870
664
|
{isWebScanningRestricted ? (<>
|
|
871
665
|
<View style={styles.restrictionWarningBanner}>
|
|
872
|
-
<Text style={styles.restrictionWarningTitle}>
|
|
873
|
-
|
|
874
|
-
</Text>
|
|
875
|
-
<Text style={styles.restrictionWarningText}>
|
|
876
|
-
{state.currentLanguage === 'en'
|
|
877
|
-
? `This document can only be scanned using our mobile app.`
|
|
878
|
-
: `Ce document ne peut être scanné qu'avec notre application mobile.`}
|
|
879
|
-
</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>
|
|
880
668
|
</View>
|
|
881
|
-
|
|
882
669
|
<Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="primary" size="large" fullWidth/>
|
|
883
|
-
</>) : (
|
|
884
|
-
|
|
885
|
-
<>
|
|
886
|
-
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => {
|
|
887
|
-
setShowCamera(true);
|
|
888
|
-
refreshCamera();
|
|
889
|
-
actions.showCustomStepper(false);
|
|
890
|
-
}} variant="primary" size="large" fullWidth/>
|
|
891
|
-
|
|
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/>
|
|
892
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/>)}
|
|
893
673
|
</>)}
|
|
894
|
-
|
|
895
674
|
</View>)}
|
|
896
|
-
|
|
897
675
|
{capturedImages[currentSide]?.dir && (<>
|
|
898
676
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
899
677
|
<Button title={t('common.next')} onPress={() => {
|
|
@@ -922,66 +700,66 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
922
700
|
</View>
|
|
923
701
|
|
|
924
702
|
{showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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'
|
|
932
710
|
? "Scan the QR code to download our mobile app and complete the scanning process securely."
|
|
933
711
|
: "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
|
|
934
|
-
|
|
935
|
-
|
|
712
|
+
</Text>
|
|
713
|
+
</View>
|
|
936
714
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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>
|
|
949
727
|
|
|
950
|
-
|
|
951
|
-
|
|
728
|
+
<View style={styles.qrCodeWrapper}>
|
|
729
|
+
<QRCode value={qrPlatform === 'android'
|
|
952
730
|
? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
|
|
953
731
|
: 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'} size={180} backgroundColor="#FFFFFF" color="#0F172A"/>
|
|
954
|
-
|
|
732
|
+
</View>
|
|
955
733
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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>
|
|
967
745
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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>)}
|
|
981
759
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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>)}
|
|
985
763
|
</View>);
|
|
986
764
|
};
|
|
987
765
|
const styles = StyleSheet.create({
|