@sanctum-key/react-native-sdk 1.0.37 → 1.0.38

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