@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.
@@ -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, Alert } from 'react-native';
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 { logger } from '../../utils/logger';
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
- "NB": "New Brunswick", "NL": "Newfoundland and Labrador",
46
- "NT": "Northwest Territories", "NS": "Nova Scotia", "NU": "Nunavut",
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
- // Ref mirrors — prevent stale closure reads in handleSilentCapture on iOS
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
- const [generatedCode, setGeneratedCode] = useState<string | null>(null);
105
- const [isGeneratingCode, setIsGeneratingCode] = useState(false);
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
- setCapturedImages((prev) => {
209
- const newState = { ...prev };
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
- if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
245
- const updatedImages = value as Record<string, IIDCardPayload>;
246
- setCapturedImages(updatedImages);
247
- const currentImageData = updatedImages[currentSide];
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
- ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
271
- : getLocalizedText(component.instructions as Record<string, LocalizedText>);
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
- setProcessingImagePath(null);
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
- refreshCamera();
327
- actions.showCustomStepper(false);
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 = (currentSide: string, countryKey: string) => {
342
- const rawDocType = countrySelectionData?.documentType;
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[countryData?.code || ''] || countryData?.code || countryKey;
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 mrzTypes: string[] = [];
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[currentSide]) authMethod.push(item[currentSide]);
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
- minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
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
- ...capturedImages,
430
- // Store safeCroppedPath (guaranteed file:// on iOS) as dir so
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
- setProcessingImagePath(null);
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 (currentSide !== 'back') return false;
469
- if (!countryData?.code || !selectedDocumentType?.type) return false;
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[countryData.code] || countryData.code;
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() === countryData.code.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 = countryData.region?.trim()?.length > 0 ? countryData.region.trim() : 'root';
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 (isProcessingCapture || !pendingCAImage) return;
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 isHIC = selectedDocumentType!.type === 'health_insurance_card';
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
- templatePath: pendingCAImage.templatePath,
527
- bbox: pendingCAImage.bbox,
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
- console.error("Barcode processing failed", error);
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
- ? `file://${result.path}`
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 (!selectedDocumentType) {
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 = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
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[countryData?.code || ''] || 'root');
621
- const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
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
- if (currentSide === 'front') {
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 = capturedImages['front']?.templatePath || '';
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 docTypeStr = selectedDocumentType!.type;
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
- const verifiedResult: ISilentCaptureResult & { region?: string } = {
686
- path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
687
- mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
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, [currentSide]: bbox }));
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
- console.log("Backend Verification Error:", error);
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
- userFriendlyMessage = state.currentLanguage === 'en'
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
- userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
746
- } else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
747
- userFriendlyMessage = 'The document does not match your selected country.';
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
- let friendlyMessage = state.currentLanguage === 'en'
777
- ? 'An unknown camera error occurred.'
778
- : 'Une erreur de caméra inconnue est survenue.';
779
-
780
- // 1. Detect Hardware Lock (Camera in use by Zoom, Teams, or another app)
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
- onSilentCapture={handleSilentCapture}
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
- <IdCardOverlay
923
- xMin={cameraConfig.overlay.bbox.xMin}
924
- yMin={cameraConfig.overlay.bbox.yMin}
925
- xMax={cameraConfig.overlay.bbox.xMax}
926
- yMax={cameraConfig.overlay.bbox.yMax}
927
- instructions={cameraConfig.overlay.guideText}
928
- cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
929
- isSuccess={silentCaptureResult.success || scanPhase === 'barcode'}
930
- language={state.currentLanguage}
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
- actions.previousComponent();
617
+ setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
951
618
  }
952
- },
953
- selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
954
- step: state.currentComponentIndex + 1,
955
- totalSteps: state.template.components.length,
956
- side: currentSide,
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
- {state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}
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
- <Text style={styles.webSecondaryButtonText}>Cancel</Text>
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
- <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
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
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
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
- {state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}
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
- title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
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
- showAlert('Error', 'Document type not selected');
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
- <View style={styles.qrModalOverlay}>
1149
- <View style={styles.qrModalContainer}>
1150
- <View style={styles.qrModalHeader}>
1151
- <Text style={styles.qrModalTitle}>
1152
- {state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
1153
- </Text>
1154
- <Text style={styles.qrModalText}>
1155
- {state.currentLanguage === 'en'
1156
- ? "Scan the QR code to download our mobile app and complete the scanning process securely."
1157
- : "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
1158
- </Text>
1159
- </View>
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
- <View style={styles.qrToggleContainer}>
1162
- <TouchableOpacity
1163
- style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]}
1164
- onPress={() => setQrPlatform('android')}
1165
- >
1166
- <Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
1167
- Android
1168
- </Text>
1169
- </TouchableOpacity>
1170
- <TouchableOpacity
1171
- style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]}
1172
- onPress={() => setQrPlatform('ios')}
1173
- >
1174
- <Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
1175
- iOS (Apple)
1176
- </Text>
1177
- </TouchableOpacity>
1178
- </View>
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
- <View style={styles.qrCodeWrapper}>
1181
- <QRCode
1182
- value={
1183
- qrPlatform === 'android'
1184
- ? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
1185
- : 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'
1186
- }
1187
- size={180}
1188
- backgroundColor="#FFFFFF"
1189
- color="#0F172A"
1190
- />
1191
- </View>
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
- <View style={styles.linkCodeSection}>
1194
- <Text style={styles.linkCodeHeader}>
1195
- {state.currentLanguage === 'en' ? 'Use code below to proceed on the Sanctum Key App.' : 'Vous avez déjà l\'application ? Liez-la :'}
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
- {isWebScanningRestricted && (
1214
- <View style={styles.storeLinksContainer}>
1215
- <Text style={styles.storeLinksTitle}>
1216
- {state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
1217
- </Text>
1218
- <View style={styles.storeButtonsRow}>
1219
- <TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')} >
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
- </View>
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
  };