@sanctum-key/react-native-sdk 1.0.18 → 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 (48) 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/CountrySelectionTemplate.d.ts.map +1 -1
  7. package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
  8. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  9. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  10. package/build/src/components/KYCElements/IDCardCapture.js +224 -189
  11. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  12. package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/PhoneVerificationTemplate.js +0 -2
  14. package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
  15. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  16. package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
  17. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  18. package/build/src/components/OverLay/IdCard.d.ts +6 -1
  19. package/build/src/components/OverLay/IdCard.d.ts.map +1 -1
  20. package/build/src/components/OverLay/IdCard.js +36 -34
  21. package/build/src/components/OverLay/IdCard.js.map +1 -1
  22. package/build/src/config/countriesData.d.ts.map +1 -1
  23. package/build/src/config/countriesData.js.map +1 -1
  24. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  25. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  26. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  27. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  28. package/build/src/modules/api/CardAuthentification.js +67 -14
  29. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  30. package/build/src/modules/api/KYCService.d.ts +10 -0
  31. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  32. package/build/src/modules/api/KYCService.js +135 -49
  33. package/build/src/modules/api/KYCService.js.map +1 -1
  34. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  35. package/build/src/web/WebKYCEntry.js +34 -6
  36. package/build/src/web/WebKYCEntry.js.map +1 -1
  37. package/package.json +3 -2
  38. package/src/components/EnhancedCameraView.tsx +28 -219
  39. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  40. package/src/components/KYCElements/IDCardCapture.tsx +606 -570
  41. package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
  42. package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
  43. package/src/components/OverLay/IdCard.tsx +48 -36
  44. package/src/config/countriesData.ts +0 -4
  45. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  46. package/src/modules/api/CardAuthentification.ts +133 -67
  47. package/src/modules/api/KYCService.ts +189 -66
  48. package/src/web/WebKYCEntry.tsx +42 -7
@@ -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,33 @@ 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
+ const boxWidthPercent = isLandscape ? 0.40 : 0.85;
88
+ xMin = isLandscape ? 30 : 7.5;
89
+ xMax = xMin + (boxWidthPercent * 100);
90
+ const containerWidth = Math.min(screenWidth * 0.95, 1000);
91
+ const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
92
+ const boxPixelWidth = containerWidth * boxWidthPercent;
93
+ const boxPixelHeight = boxPixelWidth / 1.59;
94
+ const heightPercent = (boxPixelHeight / containerHeight) * 100;
95
+ yMin = (100 - heightPercent) / 2.5;
96
+ yMax = yMin + heightPercent;
97
+ }
98
+ else {
99
+ xMin = 5;
100
+ xMax = 95;
101
+ yMin = 34;
102
+ yMax = 66;
103
+ }
104
+ const platformBbox = { xMin, yMin, xMax, yMax, borderColor: '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
76
105
  return {
77
106
  cameraType: Platform.OS === 'web' ? cameraType : 'back',
78
107
  flashMode: 'auto',
79
- overlay: {
80
- guideText: instructions,
81
- bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
82
- }
108
+ overlay: { guideText: instructions, bbox: platformBbox }
83
109
  };
84
- }, [selectedDocumentType, locale, component.instructions, cameraType]);
110
+ }, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
85
111
  const retakePicture = (sideToRetake) => {
86
112
  setIsProcessingCapture(false);
87
113
  setProcessingImagePath(null);
@@ -165,9 +191,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
165
191
  height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
166
192
  };
167
193
  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);
194
+ imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
171
195
  }
172
196
  catch (e) {
173
197
  console.warn("Crop failed, falling back to original image", e);
@@ -184,6 +208,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
184
208
  ...newImages,
185
209
  country: verified.country,
186
210
  documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
211
+ ...(verified.region ? { region: verified.region } : {})
187
212
  });
188
213
  }
189
214
  setTimeout(() => {
@@ -215,9 +240,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
215
240
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
216
241
  return;
217
242
  }
243
+ const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
218
244
  try {
219
245
  if (!templatePath) {
220
- 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);
221
253
  templateResponse = templateType;
222
254
  if (templateType.template_path)
223
255
  templatePath = templateType.template_path;
@@ -240,19 +272,51 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
240
272
  if (currentSide === 'front') {
241
273
  const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
242
274
  const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
243
- 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);
244
285
  }
245
286
  else {
246
287
  let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
247
288
  if (!matchedBackAuthMethod && currentSide === 'back') {
248
289
  matchedBackAuthMethod = 'MRZ';
249
290
  }
250
- const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
251
- 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);
291
+ const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
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);
252
306
  }
253
307
  const bbox = verificationRes?.bbox || templateBbox;
254
308
  const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
255
- 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
+ };
256
320
  setSilentCaptureResult(verifiedResult);
257
321
  if (bbox)
258
322
  setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
@@ -260,7 +324,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
260
324
  }
261
325
  catch (error) {
262
326
  console.log("Backend Verification Error:", error);
263
- // 2. Define a map of technical keywords to user-friendly messages
264
327
  const rawMessage = (error?.message || '').toLowerCase();
265
328
  let userFriendlyMessage = 'Verification failed. Please try again.';
266
329
  if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
@@ -270,16 +333,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
270
333
  userFriendlyMessage = 'The document does not match your selected country.';
271
334
  }
272
335
  else if (rawMessage.includes('card_not_fully_in_frame')) {
273
- // 🚨 REMOVED 'too far' CHECK FROM HERE
274
336
  userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
275
337
  }
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
- }));
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
+ }
341
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
283
342
  }
284
343
  }
285
344
  };
@@ -293,48 +352,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
293
352
  }, [showCamera]);
294
353
  if (!countrySelectionData || !selectedDocumentType) {
295
354
  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>);
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>
361
+ </View>);
303
362
  }
304
- // --- CAMERA RENDER ---
305
363
  if (showCamera) {
306
364
  const isBusy = isProcessingCapture;
307
365
  if (isRebootingCamera) {
308
366
  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>);
367
+ <ActivityIndicator size="large" color="#2DBD60"/>
368
+ <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
369
+ </View>);
312
370
  }
313
371
  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={{
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={<>
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={{
338
393
  back: () => {
339
394
  if (currentSide === 'back') {
340
395
  setCurrentSide('front');
@@ -356,83 +411,76 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
356
411
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
357
412
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
358
413
  }}/>
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>);
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>
461
+ </View>);
410
462
  }
411
- // --- PREVIEW RENDER ---
412
463
  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={() => {
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/>
483
+ <Button title={t('common.next')} onPress={() => {
436
484
  if (!selectedDocumentType) {
437
485
  showAlert('Error', 'Document type not selected');
438
486
  return;
@@ -449,80 +497,67 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
449
497
  setProcessingImagePath(null);
450
498
  }
451
499
  }} variant="primary" size="large" fullWidth/>
452
- </>)}
453
- </View>
454
- </View>
455
- </ScrollView>
456
- </View>
457
- </View>);
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>)}
519
+ </View>);
458
520
  };
459
521
  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', },
522
+ root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
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 } }) },
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' } }) },
525
+ headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
478
526
  stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
479
527
  stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
480
528
  cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
481
- camera: { flex: 1, },
482
- // MOBILE: Escape Hatch layout
483
529
  escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
484
530
  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
531
  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 },
532
+ webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 24, paddingBottom: 24, paddingTop: 30, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
533
+ webActionButtonsRow: { flexDirection: 'row', justifyContent: 'center', gap: 16, marginTop: 12, maxWidth: 500, alignSelf: 'center', width: '100%' },
489
534
  webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
490
535
  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' },
536
+ 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
537
  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)' },
538
+ webTopControls: { position: 'absolute', top: Platform.OS === 'ios' ? 70 : 20, right: 20, zIndex: 9999 },
539
+ 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
540
  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
- },
541
+ 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
542
  previewItemContainer: { flexGrow: 1, },
502
543
  title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
503
544
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
504
545
  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
- },
546
+ sideTitle: { fontSize: 22, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
547
+ 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
548
  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
- },
549
+ 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
550
  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 },
551
+ processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
522
552
  processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
523
553
  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' }
554
+ topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
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 },
556
+ analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
557
+ qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
558
+ 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, },
559
+ qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
560
+ qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
561
+ qrCodeWrapper: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#E2E8F0', marginBottom: 24, }
527
562
  });
528
563
  //# sourceMappingURL=IDCardCapture.js.map