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