@sanctum-key/react-native-sdk 1.0.19 → 1.0.20

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.
Files changed (31) hide show
  1. package/build/package.json +1 -1
  2. package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  3. package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
  4. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  5. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  6. package/build/src/components/KYCElements/IDCardCapture.js +166 -129
  7. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  8. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  9. package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
  10. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  11. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  12. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  13. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  14. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  15. package/build/src/modules/api/CardAuthentification.js +67 -13
  16. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  17. package/build/src/modules/api/KYCService.d.ts +10 -0
  18. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  19. package/build/src/modules/api/KYCService.js +94 -25
  20. package/build/src/modules/api/KYCService.js.map +1 -1
  21. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  22. package/build/src/web/WebKYCEntry.js +34 -6
  23. package/build/src/web/WebKYCEntry.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  26. package/src/components/KYCElements/IDCardCapture.tsx +613 -556
  27. package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
  28. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  29. package/src/modules/api/CardAuthentification.ts +133 -66
  30. package/src/modules/api/KYCService.ts +142 -38
  31. package/src/web/WebKYCEntry.tsx +42 -7
@@ -84,18 +84,14 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
84
84
  const isLandscape = screenWidth > screenHeight;
85
85
  let xMin, xMax, yMin, yMax;
86
86
  if (Platform.OS === 'web') {
87
- // 🚨 UI FIX: Differentiate Desktop Web (Landscape) vs Mobile Web (Portrait)
88
87
  const boxWidthPercent = isLandscape ? 0.40 : 0.85;
89
- // On wide desktop, shift slightly right. On mobile portrait, center perfectly.
90
88
  xMin = isLandscape ? 30 : 7.5;
91
89
  xMax = xMin + (boxWidthPercent * 100);
92
- // Safely calculate height to guarantee a 1.59 aspect ratio
93
90
  const containerWidth = Math.min(screenWidth * 0.95, 1000);
94
91
  const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
95
92
  const boxPixelWidth = containerWidth * boxWidthPercent;
96
93
  const boxPixelHeight = boxPixelWidth / 1.59;
97
94
  const heightPercent = (boxPixelHeight / containerHeight) * 100;
98
- // Center vertically
99
95
  yMin = (100 - heightPercent) / 2.5;
100
96
  yMax = yMin + heightPercent;
101
97
  }
@@ -212,6 +208,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
212
208
  ...newImages,
213
209
  country: verified.country,
214
210
  documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
211
+ ...(verified.region ? { region: verified.region } : {})
215
212
  });
216
213
  }
217
214
  setTimeout(() => {
@@ -243,9 +240,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
243
240
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
244
241
  return;
245
242
  }
243
+ const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
246
244
  try {
247
245
  if (!templatePath) {
248
- const templateType = await checkTemplateType({ path: result.path || '', docType: selectedDocumentType?.type, docRegion: countryData?.code || "", postfix: currentSide }, env);
246
+ const templateType = await checkTemplateType({
247
+ path: result.path || '',
248
+ docType: selectedDocumentType?.type,
249
+ docRegion: countryData?.code || "",
250
+ postfix: currentSide,
251
+ issuingSubdivision: issuingSubdivision
252
+ }, env);
249
253
  templateResponse = templateType;
250
254
  if (templateType.template_path)
251
255
  templatePath = templateType.template_path;
@@ -268,7 +272,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
268
272
  if (currentSide === 'front') {
269
273
  const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
270
274
  const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
271
- verificationRes = await frontVerification({ path: result.path, regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '', code: countryData?.code || '', currentSide, templatePath, mrzType }, env);
275
+ verificationRes = await frontVerification({
276
+ path: result.path,
277
+ regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes },
278
+ selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
279
+ code: countryData?.code || '',
280
+ currentSide,
281
+ templatePath,
282
+ mrzType,
283
+ issuingSubdivision: issuingSubdivision
284
+ }, env);
272
285
  }
273
286
  else {
274
287
  let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
@@ -276,11 +289,34 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
276
289
  matchedBackAuthMethod = 'MRZ';
277
290
  }
278
291
  const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
279
- verificationRes = await backVerification({ path: result.path, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '', code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse }, env);
292
+ // 🚨 EXTRACT FRONT TEMPLATE TO VALIDATE AGAINST BACK TEMPLATE
293
+ const frontTemplatePath = capturedImages['front']?.templatePath || '';
294
+ verificationRes = await backVerification({
295
+ path: result.path,
296
+ regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes },
297
+ selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
298
+ code: countryData?.code || '',
299
+ currentSide,
300
+ templatePath,
301
+ mrzType: backMrzType,
302
+ templateResponse,
303
+ issuingSubdivision: issuingSubdivision,
304
+ frontTemplatePath: frontTemplatePath // 🚨 PASSED DOWN TO TRIGGER VALIDATION
305
+ }, env);
280
306
  }
281
307
  const bbox = verificationRes?.bbox || templateBbox;
282
308
  const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
283
- const verifiedResult = { path: result.path, templatePath, bbox, success: true, mrz, isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type };
309
+ const verifiedResult = {
310
+ path: result.path,
311
+ templatePath,
312
+ bbox,
313
+ success: true,
314
+ mrz,
315
+ isAnalyzing: false,
316
+ country: countryData?.code,
317
+ documentType: selectedDocumentType.type,
318
+ region: issuingSubdivision
319
+ };
284
320
  setSilentCaptureResult(verifiedResult);
285
321
  if (bbox)
286
322
  setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
@@ -299,6 +335,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
299
335
  else if (rawMessage.includes('card_not_fully_in_frame')) {
300
336
  userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
301
337
  }
338
+ else if (rawMessage.includes('ne correspond pas au recto')) { // 🚨 New Security Error Handler
339
+ userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
340
+ }
302
341
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
303
342
  }
304
343
  }
@@ -312,44 +351,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
312
351
  actions.showCustomStepper(!showCamera);
313
352
  }, [showCamera]);
314
353
  if (!countrySelectionData || !selectedDocumentType) {
315
- return (<View style={styles.root}>
316
- <View style={styles.previewContainer}>
317
- <Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
318
- <Text style={styles.description}>
319
- {state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
320
- </Text>
321
- </View>
354
+ return (<View style={styles.root}>
355
+ <View style={styles.previewContainer}>
356
+ <Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
357
+ <Text style={styles.description}>
358
+ {state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
359
+ </Text>
360
+ </View>
322
361
  </View>);
323
362
  }
324
363
  if (showCamera) {
325
364
  const isBusy = isProcessingCapture;
326
365
  if (isRebootingCamera) {
327
- return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
328
- <ActivityIndicator size="large" color="#2DBD60"/>
329
- <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
366
+ return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
367
+ <ActivityIndicator size="large" color="#2DBD60"/>
368
+ <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
330
369
  </View>);
331
370
  }
332
- return (<View style={styles.root}>
333
- <View style={[styles.cameraWrapper, { flex: 1 }]}>
334
- <View style={styles.headerContainer}>
335
- <Text style={styles.headerTitle}>
336
- {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
337
- </Text>
338
- <View style={styles.stepBadge}>
339
- <Text style={styles.stepText}>
340
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
341
- </Text>
342
- </View>
343
- </View>
344
-
345
- <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
346
- {Platform.OS === 'web' && (<View style={styles.webTopControls}>
347
- <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
348
- <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
349
- </TouchableOpacity>
350
- </View>)}
351
-
352
- <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
371
+ return (<View style={styles.root}>
372
+ <View style={[styles.cameraWrapper, { flex: 1 }]}>
373
+ <View style={styles.headerContainer}>
374
+ <Text style={styles.headerTitle}>
375
+ {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
376
+ </Text>
377
+ <View style={styles.stepBadge}>
378
+ <Text style={styles.stepText}>
379
+ {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
380
+ </Text>
381
+ </View>
382
+ </View>
383
+
384
+ <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
385
+ {Platform.OS === 'web' && (<View style={styles.webTopControls}>
386
+ <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
387
+ <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
388
+ </TouchableOpacity>
389
+ </View>)}
390
+
391
+ <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
353
392
  <IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success} language={state.currentLanguage} stepperProps={{
354
393
  back: () => {
355
394
  if (currentSide === 'back') {
@@ -371,76 +410,76 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
371
410
  },
372
411
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
373
412
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
374
- }}/>
375
- </>}/>
376
-
377
- {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
378
- <View style={styles.topAnalyzingPill}>
379
- <ActivityIndicator size="small" color="white"/>
380
- <Text style={styles.analyzingPillText}>
381
- {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
382
- </Text>
383
- </View>
384
- </View>)}
385
-
386
- {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
387
- {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
388
- <View style={styles.processingOverlay}>
389
- <ActivityIndicator size="large" color="#2DBD60"/>
390
- <Text style={styles.processingText}>
391
- {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
392
- </Text>
393
- </View>
394
- </View>)}
395
-
396
- {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
397
- {silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
398
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
399
- </View>)}
400
- <View style={styles.webActionButtonsRow}>
401
- <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
402
- <Text style={styles.webSecondaryButtonText}>Cancel</Text>
403
- </TouchableOpacity>
404
- <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
405
- <Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
406
- </TouchableOpacity>
407
- </View>
408
- </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
409
- <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
410
- <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
411
- </TouchableOpacity>
412
- <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
413
- <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
414
- </TouchableOpacity>
415
- </View>) : null}
416
-
417
- {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
418
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
419
- </View>) : null}
420
- </View>
421
- </View>
413
+ }}/>
414
+ </>}/>
415
+
416
+ {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
417
+ <View style={styles.topAnalyzingPill}>
418
+ <ActivityIndicator size="small" color="white"/>
419
+ <Text style={styles.analyzingPillText}>
420
+ {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
421
+ </Text>
422
+ </View>
423
+ </View>)}
424
+
425
+ {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
426
+ {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
427
+ <View style={styles.processingOverlay}>
428
+ <ActivityIndicator size="large" color="#2DBD60"/>
429
+ <Text style={styles.processingText}>
430
+ {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
431
+ </Text>
432
+ </View>
433
+ </View>)}
434
+
435
+ {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
436
+ {silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
437
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
438
+ </View>)}
439
+ <View style={styles.webActionButtonsRow}>
440
+ <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
441
+ <Text style={styles.webSecondaryButtonText}>Cancel</Text>
442
+ </TouchableOpacity>
443
+ <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
444
+ <Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
445
+ </TouchableOpacity>
446
+ </View>
447
+ </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
448
+ <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
449
+ <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
450
+ </TouchableOpacity>
451
+ <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
452
+ <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
453
+ </TouchableOpacity>
454
+ </View>) : null}
455
+
456
+ {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
457
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
458
+ </View>) : null}
459
+ </View>
460
+ </View>
422
461
  </View>);
423
462
  }
424
- return (<View style={styles.root}>
425
- <View style={styles.previewContainer}>
426
- <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
427
- <View key={currentSide} style={styles.sideContainer}>
428
- <Text style={styles.sideTitle}>
429
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
430
- </Text>
431
- <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
432
- {getLocalizedText(component.instructions)}
433
- </Text>
434
- <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
435
- <View style={styles.imagePreviewWrapper}>
436
- {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
437
- </View>
438
- {!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
439
- <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
440
- {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/>)}
441
- </View>)}
442
- {capturedImages[currentSide]?.dir && (<>
443
- <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
463
+ return (<View style={styles.root}>
464
+ <View style={styles.previewContainer}>
465
+ <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
466
+ <View key={currentSide} style={styles.sideContainer}>
467
+ <Text style={styles.sideTitle}>
468
+ {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
469
+ </Text>
470
+ <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
471
+ {getLocalizedText(component.instructions)}
472
+ </Text>
473
+ <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
474
+ <View style={styles.imagePreviewWrapper}>
475
+ {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
476
+ </View>
477
+ {!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
478
+ <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
479
+ {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/>)}
480
+ </View>)}
481
+ {capturedImages[currentSide]?.dir && (<>
482
+ <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
444
483
  <Button title={t('common.next')} onPress={() => {
445
484
  if (!selectedDocumentType) {
446
485
  showAlert('Error', 'Document type not selected');
@@ -457,32 +496,31 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
457
496
  setIsProcessingCapture(false);
458
497
  setProcessingImagePath(null);
459
498
  }
460
- }} variant="primary" size="large" fullWidth/>
461
- </>)}
462
- </View>
463
- </View>
464
- </ScrollView>
465
- </View>
466
- {showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
467
- <View style={styles.qrModalContainer}>
468
- <Text style={styles.qrModalTitle}>
469
- {state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
470
- </Text>
471
- <Text style={styles.qrModalText}>
472
- {state.currentLanguage === 'en' ? "Point your phone's camera at this QR code to seamlessly continue the process on your mobile device." : "Pointez l'appareil photo de votre téléphone vers ce code QR pour continuer le processus en toute fluidité sur votre appareil mobile."}
473
- </Text>
474
- <View style={styles.qrCodeWrapper}>
475
- <QRCode value={currentUrl} size={220} backgroundColor="transparent"/>
476
- </View>
477
- <Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
478
- </View>
479
- </View>)}
499
+ }} variant="primary" size="large" fullWidth/>
500
+ </>)}
501
+ </View>
502
+ </View>
503
+ </ScrollView>
504
+ </View>
505
+ {showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
506
+ <View style={styles.qrModalContainer}>
507
+ <Text style={styles.qrModalTitle}>
508
+ {state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
509
+ </Text>
510
+ <Text style={styles.qrModalText}>
511
+ {state.currentLanguage === 'en' ? "Point your phone's camera at this QR code to seamlessly continue the process on your mobile device." : "Pointez l'appareil photo de votre téléphone vers ce code QR pour continuer le processus en toute fluidité sur votre appareil mobile."}
512
+ </Text>
513
+ <View style={styles.qrCodeWrapper}>
514
+ <QRCode value={currentUrl} size={220} backgroundColor="transparent"/>
515
+ </View>
516
+ <Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
517
+ </View>
518
+ </View>)}
480
519
  </View>);
481
520
  };
482
521
  const styles = StyleSheet.create({
483
522
  root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
484
523
  cameraWrapper: { width: '100%', backgroundColor: '#000000', overflow: 'hidden', ...Platform.select({ web: { maxWidth: 1000, width: '95%', height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 30, elevation: 24 }, default: { flex: 1 } }) },
485
- // 🚨 UI FIX: flexWrap handles overflowing text perfectly on narrow mobile screens
486
524
  headerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 10, paddingHorizontal: 20, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10, ...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } }) },
487
525
  headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
488
526
  stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
@@ -513,7 +551,6 @@ const styles = StyleSheet.create({
513
551
  processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
514
552
  processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
515
553
  errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
516
- // 🚨 UI FIX: Dropped the pill slightly lower so it never overlaps the flip lens button
517
554
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
518
555
  topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.7)', paddingVertical: 10, paddingHorizontal: 20, borderRadius: 30, gap: 10, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
519
556
  analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },