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

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/README.md +1 -1
  2. package/build/package.json +3 -2
  3. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  4. package/build/src/components/EnhancedCameraView.js +19 -182
  5. package/build/src/components/EnhancedCameraView.js.map +1 -1
  6. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  7. package/build/src/components/KYCElements/IDCardCapture.js +189 -191
  8. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  9. package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
  10. package/build/src/components/KYCElements/PhoneVerificationTemplate.js +0 -2
  11. package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
  12. package/build/src/components/OverLay/IdCard.d.ts +6 -1
  13. package/build/src/components/OverLay/IdCard.d.ts.map +1 -1
  14. package/build/src/components/OverLay/IdCard.js +36 -34
  15. package/build/src/components/OverLay/IdCard.js.map +1 -1
  16. package/build/src/config/countriesData.d.ts.map +1 -1
  17. package/build/src/config/countriesData.js.map +1 -1
  18. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  19. package/build/src/modules/api/CardAuthentification.js +0 -1
  20. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  21. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  22. package/build/src/modules/api/KYCService.js +41 -24
  23. package/build/src/modules/api/KYCService.js.map +1 -1
  24. package/package.json +3 -2
  25. package/src/components/EnhancedCameraView.tsx +28 -219
  26. package/src/components/KYCElements/IDCardCapture.tsx +560 -581
  27. package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
  28. package/src/components/OverLay/IdCard.tsx +48 -36
  29. package/src/config/countriesData.ts +0 -4
  30. package/src/modules/api/CardAuthentification.ts +0 -1
  31. package/src/modules/api/KYCService.ts +48 -29
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react';
2
- import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
2
+ import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions } from 'react-native';
3
3
  import { showAlert } from '../../utils/platformAlert';
4
4
  import { EnhancedCameraView } from '../EnhancedCameraView';
5
5
  import { GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
@@ -13,6 +13,7 @@ import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
13
13
  import pathToBase64 from '../../utils/pathToBase64';
14
14
  import { cropByObb, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
15
15
  import REGION_MAPPING from '../../config/region_mapping.json';
16
+ import QRCode from 'react-native-qrcode-svg';
16
17
  const ISO_TO_COUNTRY_NAME = { 'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada', 'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain', 'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali' };
17
18
  export const IDCardCapture = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
18
19
  const { t, locale } = useI18n();
@@ -25,10 +26,17 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
25
26
  const [processingImagePath, setProcessingImagePath] = useState(null);
26
27
  const [cameraKey, setCameraKey] = useState(0);
27
28
  const [isRebootingCamera, setIsRebootingCamera] = useState(false);
28
- // Web specific state
29
29
  const [cameraType, setCameraType] = useState('back');
30
+ const [showQRModal, setShowQRModal] = useState(false);
31
+ const [currentUrl, setCurrentUrl] = useState('');
32
+ useEffect(() => {
33
+ if (typeof window !== 'undefined' && window.location) {
34
+ setCurrentUrl(window.location.href);
35
+ }
36
+ }, []);
30
37
  const documentTypeMapping = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
31
38
  const { actions, state, env } = useTemplateKYCFlowContext();
39
+ const { width: screenWidth, height: screenHeight } = useWindowDimensions();
32
40
  const getLocalizedText = (text) => {
33
41
  if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
34
42
  return text[currentSide][locale] || '';
@@ -73,15 +81,37 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
73
81
  }, [value, currentSide]);
74
82
  const cameraConfig = useMemo(() => {
75
83
  const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
84
+ const isLandscape = screenWidth > screenHeight;
85
+ let xMin, xMax, yMin, yMax;
86
+ if (Platform.OS === 'web') {
87
+ // 🚨 UI FIX: Differentiate Desktop Web (Landscape) vs Mobile Web (Portrait)
88
+ const boxWidthPercent = isLandscape ? 0.40 : 0.85;
89
+ // On wide desktop, shift slightly right. On mobile portrait, center perfectly.
90
+ xMin = isLandscape ? 30 : 7.5;
91
+ xMax = xMin + (boxWidthPercent * 100);
92
+ // Safely calculate height to guarantee a 1.59 aspect ratio
93
+ const containerWidth = Math.min(screenWidth * 0.95, 1000);
94
+ const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
95
+ const boxPixelWidth = containerWidth * boxWidthPercent;
96
+ const boxPixelHeight = boxPixelWidth / 1.59;
97
+ const heightPercent = (boxPixelHeight / containerHeight) * 100;
98
+ // Center vertically
99
+ yMin = (100 - heightPercent) / 2.5;
100
+ yMax = yMin + heightPercent;
101
+ }
102
+ else {
103
+ xMin = 5;
104
+ xMax = 95;
105
+ yMin = 34;
106
+ yMax = 66;
107
+ }
108
+ const platformBbox = { xMin, yMin, xMax, yMax, borderColor: '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
76
109
  return {
77
110
  cameraType: Platform.OS === 'web' ? cameraType : 'back',
78
111
  flashMode: 'auto',
79
- overlay: {
80
- guideText: instructions,
81
- bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
82
- }
112
+ overlay: { guideText: instructions, bbox: platformBbox }
83
113
  };
84
- }, [selectedDocumentType, locale, component.instructions, cameraType]);
114
+ }, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
85
115
  const retakePicture = (sideToRetake) => {
86
116
  setIsProcessingCapture(false);
87
117
  setProcessingImagePath(null);
@@ -165,9 +195,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
165
195
  height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
166
196
  };
167
197
  try {
168
- // Apply the crop using the UI percentages and a tight 2% tolerance
169
- // NOTE: If you integrated the Kotlin centered crop function earlier, replace this with cropToCenterScanArea
170
- imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
198
+ imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
171
199
  }
172
200
  catch (e) {
173
201
  console.warn("Crop failed, falling back to original image", e);
@@ -247,7 +275,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
247
275
  if (!matchedBackAuthMethod && currentSide === 'back') {
248
276
  matchedBackAuthMethod = 'MRZ';
249
277
  }
250
- const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
278
+ const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
251
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);
252
280
  }
253
281
  const bbox = verificationRes?.bbox || templateBbox;
@@ -260,7 +288,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
260
288
  }
261
289
  catch (error) {
262
290
  console.log("Backend Verification Error:", error);
263
- // 2. Define a map of technical keywords to user-friendly messages
264
291
  const rawMessage = (error?.message || '').toLowerCase();
265
292
  let userFriendlyMessage = 'Verification failed. Please try again.';
266
293
  if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
@@ -270,16 +297,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
270
297
  userFriendlyMessage = 'The document does not match your selected country.';
271
298
  }
272
299
  else if (rawMessage.includes('card_not_fully_in_frame')) {
273
- // 🚨 REMOVED 'too far' CHECK FROM HERE
274
300
  userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
275
301
  }
276
- // 3. Set ONLY the friendly message to the UI state
277
- setSilentCaptureResult(prev => ({
278
- ...prev,
279
- isAnalyzing: false,
280
- success: false,
281
- error: userFriendlyMessage
282
- }));
302
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
283
303
  }
284
304
  }
285
305
  };
@@ -292,49 +312,45 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
292
312
  actions.showCustomStepper(!showCamera);
293
313
  }, [showCamera]);
294
314
  if (!countrySelectionData || !selectedDocumentType) {
295
- return (<View style={styles.root}>
296
- <View style={styles.previewContainer}>
297
- <Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
298
- <Text style={styles.description}>
299
- {state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
300
- </Text>
301
- </View>
302
- </View>);
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>
322
+ </View>);
303
323
  }
304
- // --- CAMERA RENDER ---
305
324
  if (showCamera) {
306
325
  const isBusy = isProcessingCapture;
307
326
  if (isRebootingCamera) {
308
- return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
309
- <ActivityIndicator size="large" color="#2DBD60"/>
310
- <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
311
- </View>);
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>
330
+ </View>);
312
331
  }
313
- return (<View style={styles.root}>
314
- <View style={[styles.cameraWrapper, { flex: 1 }]}>
315
-
316
- <View style={styles.headerContainer}>
317
- <Text style={styles.headerTitle}>
318
- {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
319
- </Text>
320
- <View style={styles.stepBadge}>
321
- <Text style={styles.stepText}>
322
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
323
- </Text>
324
- </View>
325
- </View>
326
-
327
- <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
328
-
329
- {/* WEB ONLY: Flip Camera Top Button */}
330
- {Platform.OS === 'web' && (<View style={styles.webTopControls}>
331
- <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
332
- <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
333
- </TouchableOpacity>
334
- </View>)}
335
-
336
- <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
337
- <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={{
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={<>
353
+ <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={{
338
354
  back: () => {
339
355
  if (currentSide === 'back') {
340
356
  setCurrentSide('front');
@@ -355,84 +371,77 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
355
371
  },
356
372
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
357
373
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
358
- }}/>
359
- </>}/>
360
-
361
- {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
362
- <View style={styles.topAnalyzingPill}>
363
- <ActivityIndicator size="small" color="white"/>
364
- <Text style={styles.analyzingPillText}>
365
- {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
366
- </Text>
367
- </View>
368
- </View>)}
369
-
370
- {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
371
- {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
372
- <View style={styles.processingOverlay}>
373
- <ActivityIndicator size="large" color="#2DBD60"/>
374
- <Text style={styles.processingText}>
375
- {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
376
- </Text>
377
- </View>
378
- </View>)}
379
-
380
- {/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
381
- {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
382
- {silentCaptureResult.error ? (<View style={styles.floatingErrorBanner}>
383
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
384
- </View>) : <View style={{ height: 10 }}/>}
385
-
386
- <View style={styles.webActionButtonsRow}>
387
- <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
388
- <Text style={styles.webSecondaryButtonText}>Cancel</Text>
389
- </TouchableOpacity>
390
- <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
391
- <Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
392
- </TouchableOpacity>
393
- </View>
394
- </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
395
- <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
396
- <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
397
- </TouchableOpacity>
398
- <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
399
- <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
400
- </TouchableOpacity>
401
- </View>) : null}
402
-
403
- {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
404
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
405
- </View>) : null}
406
-
407
- </View>
408
- </View>
409
- </View>);
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>
422
+ </View>);
410
423
  }
411
- // --- PREVIEW RENDER ---
412
- return (<View style={styles.root}>
413
- <View style={styles.previewContainer}>
414
- <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
415
- <View key={currentSide} style={styles.sideContainer}>
416
- <Text style={styles.sideTitle}>
417
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
418
- </Text>
419
- <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
420
- {getLocalizedText(component.instructions)}
421
- </Text>
422
-
423
- <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
424
-
425
- {/* 🚨 REMOVED THE ORANGE WARNING BANNER ENTIRELY */}
426
-
427
- {/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
428
- <View style={styles.imagePreviewWrapper}>
429
- {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
430
- </View>
431
-
432
- {!capturedImages[currentSide]?.dir && (<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>)}
433
- {capturedImages[currentSide]?.dir && (<>
434
- <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
435
- <Button title={t('common.next')} onPress={() => {
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/>
444
+ <Button title={t('common.next')} onPress={() => {
436
445
  if (!selectedDocumentType) {
437
446
  showAlert('Error', 'Document type not selected');
438
447
  return;
@@ -448,81 +457,70 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
448
457
  setIsProcessingCapture(false);
449
458
  setProcessingImagePath(null);
450
459
  }
451
- }} variant="primary" size="large" fullWidth/>
452
- </>)}
453
- </View>
454
- </View>
455
- </ScrollView>
456
- </View>
457
- </View>);
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>)}
480
+ </View>);
458
481
  };
459
482
  const styles = StyleSheet.create({
460
- root: {
461
- flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
462
- ...Platform.select({
463
- web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' },
464
- })
465
- },
466
- cameraWrapper: {
467
- width: '100%', backgroundColor: '#000000', overflow: 'hidden',
468
- ...Platform.select({
469
- web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 },
470
- default: { flex: 1 }
471
- })
472
- },
473
- headerContainer: {
474
- flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
475
- ...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
476
- },
477
- headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
483
+ root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
484
+ 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
+ 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
+ headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
478
488
  stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
479
489
  stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
480
490
  cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
481
- camera: { flex: 1, },
482
- // MOBILE: Escape Hatch layout
483
491
  escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
484
492
  fallbackRefreshButton: { backgroundColor: 'rgba(0, 0, 0, 0.8)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5, elevation: 8 },
485
493
  fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
486
- // WEB: Control Bar layout
487
- webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingBottom: 20, paddingTop: 20, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
488
- webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
494
+ webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 24, paddingBottom: 24, paddingTop: 30, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
495
+ webActionButtonsRow: { flexDirection: 'row', justifyContent: 'center', gap: 16, marginTop: 12, maxWidth: 500, alignSelf: 'center', width: '100%' },
489
496
  webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
490
497
  webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
491
- webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
498
+ webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.15)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
492
499
  webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
493
- // WEB: Flip Controls
494
- webTopControls: { position: 'absolute', top: 20, right: 20, zIndex: 9999 },
495
- webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
500
+ webTopControls: { position: 'absolute', top: Platform.OS === 'ios' ? 70 : 20, right: 20, zIndex: 9999 },
501
+ webFlipButton: { backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
496
502
  webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
497
- previewContainer: {
498
- width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
499
- ...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' } })
500
- },
503
+ previewContainer: { width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 30, paddingHorizontal: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 16, elevation: 8, ...Platform.select({ web: { alignSelf: 'center', maxWidth: 650 }, default: { margin: 10, width: '95%' } }) },
501
504
  previewItemContainer: { flexGrow: 1, },
502
505
  title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
503
506
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
504
507
  sideContainer: { marginBottom: 24 },
505
- sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
506
- imagePreviewWrapper: {
507
- width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
508
- ...Platform.select({
509
- web: { aspectRatio: 1.59, height: 'auto' },
510
- default: { height: 220 }
511
- })
512
- },
508
+ sideTitle: { fontSize: 22, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
509
+ imagePreviewWrapper: { width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.12, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0', ...Platform.select({ web: { aspectRatio: 1.59, height: 'auto', maxWidth: 450, alignSelf: 'center' }, default: { height: 220 } }) },
513
510
  previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
514
- floatingErrorBanner: {
515
- backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
516
- ...Platform.select({
517
- default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
518
- })
519
- },
511
+ floatingErrorBanner: { backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%', ...Platform.select({ web: { position: 'absolute', bottom: '100%', marginBottom: 16, maxWidth: 500, alignSelf: 'center', zIndex: 100000, boxShadow: '0 4px 12px rgba(0,0,0,0.15)' }, default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 } }) },
520
512
  floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
521
- processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
513
+ processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
522
514
  processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
523
515
  errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
524
- topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
525
- topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
526
- analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
516
+ // 🚨 UI FIX: Dropped the pill slightly lower so it never overlaps the flip lens button
517
+ topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
518
+ 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
+ analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
520
+ qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
521
+ qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth: 400, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 30, elevation: 24, },
522
+ qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
523
+ qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
524
+ qrCodeWrapper: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#E2E8F0', marginBottom: 24, }
527
525
  });
528
526
  //# sourceMappingURL=IDCardCapture.js.map