@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.
@@ -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
- "NB": "New Brunswick", "NL": "Newfoundland and Labrador",
28
- "NT": "Northwest Territories", "NS": "Nova Scotia", "NU": "Nunavut",
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
- // Ref mirrors — prevent stale closure reads in handleSilentCapture on iOS
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 [generatedCode, setGeneratedCode] = useState(null);
78
- const [isGeneratingCode, setIsGeneratingCode] = useState(false);
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
- if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
199
- const updatedImages = value;
200
- setCapturedImages(updatedImages);
201
- const currentImageData = updatedImages[currentSide];
202
- if (currentImageData?.dir) {
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
- ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
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
- xMin,
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 = (currentSide, countryKey) => {
283
- const rawDocType = countrySelectionData?.documentType;
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[countryData?.code || ''] || countryData?.code || countryKey;
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 = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
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[currentSide])
310
- authMethod.push(item[currentSide]);
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
- ...capturedImages,
366
- // Store safeCroppedPath (guaranteed file:// on iOS) as dir so
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 (currentSide !== 'back')
335
+ const isBarcodeRequired = (side, cData, docType) => {
336
+ if (side !== 'back')
402
337
  return false;
403
- if (!countryData?.code || !selectedDocumentType?.type)
338
+ if (!cData?.code || !docType?.type)
404
339
  return false;
405
- const rawDocType = selectedDocumentType.type;
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[countryData.code] || countryData.code;
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() === countryData.code.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 = countryData.region?.trim()?.length > 0 ? countryData.region.trim() : 'root';
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
- const requiresBarcode = regionData.some((item) => {
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 (isProcessingCapture || !pendingCAImage)
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 isHIC = selectedDocumentType.type === 'health_insurance_card';
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
- templatePath: pendingCAImage.templatePath,
454
- bbox: pendingCAImage.bbox,
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 = capturedImages[currentSide]?.templatePath || '';
415
+ let templatePath = captured[side]?.templatePath || '';
489
416
  let templateBbox;
490
417
  let templateResponse;
491
- if (!selectedDocumentType) {
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 = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
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[countryData?.code || ''] || 'root');
534
- const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
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 (currentSide === 'front') {
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 = capturedImages['front']?.templatePath || '';
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 docTypeStr = selectedDocumentType.type;
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
- const safeBbox = bbox;
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, [currentSide]: bbox }));
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
- actions.showCustomStepper(!showCamera);
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
- step: state.currentComponentIndex + 1,
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
- {state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}
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 && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
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 && (<View style={styles.floatingErrorBanner}>
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
- <Text style={styles.webSecondaryButtonText}>Cancel</Text>
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
- <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
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
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
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
- {state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}
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
- /* 🟢 NORMAL FLOW: Show "Start Scanning" and optional web handoff */
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
- <View style={styles.qrModalContainer}>
926
- <View style={styles.qrModalHeader}>
927
- <Text style={styles.qrModalTitle}>
928
- {state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
929
- </Text>
930
- <Text style={styles.qrModalText}>
931
- {state.currentLanguage === 'en'
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
- </Text>
935
- </View>
712
+ </Text>
713
+ </View>
936
714
 
937
- <View style={styles.qrToggleContainer}>
938
- <TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('android')}>
939
- <Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
940
- Android
941
- </Text>
942
- </TouchableOpacity>
943
- <TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('ios')}>
944
- <Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
945
- iOS (Apple)
946
- </Text>
947
- </TouchableOpacity>
948
- </View>
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
- <View style={styles.qrCodeWrapper}>
951
- <QRCode value={qrPlatform === 'android'
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
- </View>
732
+ </View>
955
733
 
956
- <View style={styles.linkCodeSection}>
957
- <Text style={styles.linkCodeHeader}>
958
- {state.currentLanguage === 'en' ? 'Use code below to proceed on the Sanctum Key App.' : 'Vous avez déjà l\'application ? Liez-la :'}
959
- </Text>
960
-
961
- {generatedCode ? (<ExpiringCodeTimer initialCode={generatedCode} language={state.currentLanguage}/>) : (<TouchableOpacity style={styles.generateCodeButton} onPress={handleGenerateCode} disabled={isGeneratingCode}>
962
- {isGeneratingCode ? (<ActivityIndicator size="small" color="#0F172A"/>) : (<Text style={styles.generateCodeButtonText}>
963
- {state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
964
- </Text>)}
965
- </TouchableOpacity>)}
966
- </View>
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
- {isWebScanningRestricted && (<View style={styles.storeLinksContainer}>
969
- <Text style={styles.storeLinksTitle}>
970
- {state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
971
- </Text>
972
- <View style={styles.storeButtonsRow}>
973
- <TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')}>
974
- <Text style={styles.storeButtonText}>Google Play</Text>
975
- </TouchableOpacity>
976
- <TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')}>
977
- <Text style={styles.storeButtonText}>App Store</Text>
978
- </TouchableOpacity>
979
- </View>
980
- </View>)}
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
- <Button title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
983
- </View>
984
- </View>)}
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({