@sanctum-key/react-native-sdk 1.0.24 → 1.0.26

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 (73) hide show
  1. package/build/package.json +5 -2
  2. package/build/src/components/EnhancedCameraView.d.ts +7 -1
  3. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  4. package/build/src/components/EnhancedCameraView.js +88 -30
  5. package/build/src/components/EnhancedCameraView.js.map +1 -1
  6. package/build/src/components/EnhancedCameraView.web.d.ts +5 -1
  7. package/build/src/components/EnhancedCameraView.web.d.ts.map +1 -1
  8. package/build/src/components/EnhancedCameraView.web.js +80 -295
  9. package/build/src/components/EnhancedCameraView.web.js.map +1 -1
  10. package/build/src/components/KYCElements/CameraCapture.js +1 -1
  11. package/build/src/components/KYCElements/CameraCapture.js.map +1 -1
  12. package/build/src/components/KYCElements/CountrySelection.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/CountrySelection.js +38 -218
  14. package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
  15. package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  16. package/build/src/components/KYCElements/CountrySelectionTemplate.js +5 -17
  17. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  18. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  19. package/build/src/components/KYCElements/IDCardCapture.js +509 -95
  20. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  21. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  22. package/build/src/components/KYCElements/VerificationProgressTemplate.js +93 -51
  23. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  24. package/build/src/components/OverLay/type.d.ts +1 -0
  25. package/build/src/components/OverLay/type.d.ts.map +1 -1
  26. package/build/src/components/OverLay/type.js.map +1 -1
  27. package/build/src/components/WebBarcodeScanner.d.ts +9 -0
  28. package/build/src/components/WebBarcodeScanner.d.ts.map +1 -0
  29. package/build/src/components/WebBarcodeScanner.js +152 -0
  30. package/build/src/components/WebBarcodeScanner.js.map +1 -0
  31. package/build/src/config/KYCConfigs.js +2 -2
  32. package/build/src/config/KYCConfigs.js.map +1 -1
  33. package/build/src/config/region_mapping.json +0 -14
  34. package/build/src/modules/api/CardAuthentification.d.ts +2 -0
  35. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  36. package/build/src/modules/api/CardAuthentification.js +22 -39
  37. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  38. package/build/src/modules/api/KYCService.d.ts +7 -2
  39. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  40. package/build/src/modules/api/KYCService.js +163 -57
  41. package/build/src/modules/api/KYCService.js.map +1 -1
  42. package/build/src/types/KYC.types.d.ts +1 -0
  43. package/build/src/types/KYC.types.d.ts.map +1 -1
  44. package/build/src/types/KYC.types.js.map +1 -1
  45. package/build/src/utils/aamvaParser.d.ts +62 -0
  46. package/build/src/utils/aamvaParser.d.ts.map +1 -0
  47. package/build/src/utils/aamvaParser.js +91 -0
  48. package/build/src/utils/aamvaParser.js.map +1 -0
  49. package/build/src/utils/cropByObb.d.ts.map +1 -1
  50. package/build/src/utils/cropByObb.js +4 -3
  51. package/build/src/utils/cropByObb.js.map +1 -1
  52. package/build/src/web/WebKYCEntry.d.ts +1 -1
  53. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  54. package/build/src/web/WebKYCEntry.js +105 -239
  55. package/build/src/web/WebKYCEntry.js.map +1 -1
  56. package/package.json +5 -2
  57. package/src/components/EnhancedCameraView.tsx +116 -42
  58. package/src/components/EnhancedCameraView.web.tsx +98 -338
  59. package/src/components/KYCElements/CameraCapture.tsx +1 -1
  60. package/src/components/KYCElements/CountrySelection.tsx +60 -240
  61. package/src/components/KYCElements/CountrySelectionTemplate.tsx +7 -20
  62. package/src/components/KYCElements/IDCardCapture.tsx +672 -151
  63. package/src/components/KYCElements/VerificationProgressTemplate.tsx +112 -65
  64. package/src/components/OverLay/type.ts +1 -1
  65. package/src/components/WebBarcodeScanner.tsx +273 -0
  66. package/src/config/KYCConfigs.ts +2 -2
  67. package/src/config/region_mapping.json +0 -14
  68. package/src/modules/api/CardAuthentification.ts +41 -56
  69. package/src/modules/api/KYCService.ts +236 -82
  70. package/src/types/KYC.types.ts +1 -0
  71. package/src/utils/aamvaParser.ts +102 -0
  72. package/src/utils/cropByObb.ts +5 -3
  73. package/src/web/WebKYCEntry.tsx +125 -301
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
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';
@@ -11,10 +11,24 @@ import { removeDuplicates } from '../../utils/remove-duplicate';
11
11
  import { backVerification, checkTemplateType, frontVerification } from '../../modules/api/CardAuthentification';
12
12
  import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
13
13
  import pathToBase64 from '../../utils/pathToBase64';
14
- import { cropByObb, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
14
+ import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
15
15
  import REGION_MAPPING from '../../config/region_mapping.json';
16
16
  import QRCode from 'react-native-qrcode-svg';
17
- 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
+ import { logger } from '../../utils/logger';
18
+ import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
19
+ import kycService, { authentification } from '../../modules/api/KYCService';
20
+ const ISO_TO_COUNTRY_NAME = {
21
+ 'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
22
+ 'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
23
+ 'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
24
+ };
25
+ const SHORT_TO_LONG_REGION = {
26
+ "AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba",
27
+ "NB": "New Brunswick", "NL": "Newfoundland and Labrador",
28
+ "NT": "Northwest Territories", "NS": "Nova Scotia", "NU": "Nunavut",
29
+ "ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
30
+ "SK": "Saskatchewan", "YT": "Yukon"
31
+ };
18
32
  export const IDCardCapture = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
19
33
  const { t, locale } = useI18n();
20
34
  const [showCamera, setShowCamera] = useState(false);
@@ -29,12 +43,21 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
29
43
  const [cameraType, setCameraType] = useState('back');
30
44
  const [showQRModal, setShowQRModal] = useState(false);
31
45
  const [currentUrl, setCurrentUrl] = useState('');
46
+ const [qrPlatform, setQrPlatform] = useState('android');
47
+ const [scanPhase, setScanPhase] = useState('image');
48
+ const [pendingCAImage, setPendingCAImage] = useState(null);
49
+ const scannedBarcodeRef = useRef(null);
32
50
  useEffect(() => {
33
- if (typeof window !== 'undefined' && window.location) {
51
+ if (typeof window !== 'undefined' && window.location)
34
52
  setCurrentUrl(window.location.href);
35
- }
53
+ console.log(currentUrl);
36
54
  }, []);
37
- const documentTypeMapping = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
55
+ const documentTypeMapping = {
56
+ 'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
57
+ 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
58
+ 'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
59
+ 'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
60
+ };
38
61
  const { actions, state, env } = useTemplateKYCFlowContext();
39
62
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
40
63
  const getLocalizedText = (text) => {
@@ -42,6 +65,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
42
65
  return text[currentSide][locale] || '';
43
66
  return "";
44
67
  };
68
+ useEffect(() => {
69
+ if (Platform.OS === 'web' && typeof window !== 'undefined') {
70
+ const baseUrl = window.location.origin + window.location.pathname;
71
+ const handoffUrl = `${baseUrl}?session_id=${state.session.session_id}&resume=true`;
72
+ setCurrentUrl(handoffUrl);
73
+ }
74
+ }, [state.session.session_id]);
45
75
  const countrySelectionData = useMemo(() => {
46
76
  const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
47
77
  return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
@@ -54,8 +84,49 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
54
84
  return { type: mappedType, region: countrySelectionData.region || 'root' };
55
85
  }, [countrySelectionData, documentTypeMapping]);
56
86
  const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
87
+ const isWebScanningRestricted = useMemo(() => {
88
+ if (Platform.OS !== 'web')
89
+ return false;
90
+ if (countryData?.code !== 'CA')
91
+ return false;
92
+ const docType = selectedDocumentType?.type;
93
+ return docType === 'drivers_licence' ||
94
+ docType === 'health_insurance_card' ||
95
+ docType === 'national_id' ||
96
+ docType === 'identity_card';
97
+ }, [countryData?.code, selectedDocumentType?.type]);
98
+ const targetBarcodeTypes = useMemo(() => {
99
+ const docType = selectedDocumentType?.type;
100
+ if (docType === 'health_insurance_card')
101
+ return ['code-128', 'code-39', 'pdf-417'];
102
+ return ['pdf-417'];
103
+ }, [selectedDocumentType]);
57
104
  const refreshCamera = () => {
58
105
  setIsRebootingCamera(true);
106
+ if (scanPhase === 'barcode') {
107
+ setScanPhase('image');
108
+ setPendingCAImage(null);
109
+ scannedBarcodeRef.current = null;
110
+ setCapturedImages((prev) => {
111
+ const newState = { ...prev };
112
+ delete newState[currentSide];
113
+ return newState;
114
+ });
115
+ if (value) {
116
+ const newValue = { ...value };
117
+ delete newValue[currentSide];
118
+ onValueChange(newValue);
119
+ }
120
+ setSilentCaptureResult({
121
+ success: false,
122
+ isAnalyzing: false,
123
+ path: '',
124
+ error: '',
125
+ templatePath: '',
126
+ mrz: '',
127
+ bbox: undefined
128
+ });
129
+ }
59
130
  setTimeout(() => {
60
131
  setCameraKey(prev => prev + 1);
61
132
  setIsRebootingCamera(false);
@@ -73,14 +144,24 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
73
144
  const currentImageData = updatedImages[currentSide];
74
145
  if (currentImageData?.dir) {
75
146
  setSilentCaptureResult(prev => ({
76
- ...prev, path: currentImageData.dir, success: true, isAnalyzing: false, mrz: currentImageData.mrz || '', templatePath: currentImageData.templatePath || '',
147
+ ...prev,
148
+ path: currentImageData.dir,
149
+ success: true,
150
+ isAnalyzing: false,
151
+ mrz: currentImageData.mrz || '',
152
+ templatePath: currentImageData.templatePath || '',
77
153
  }));
78
154
  }
79
155
  }
80
156
  }
81
157
  }, [value, currentSide]);
82
158
  const cameraConfig = useMemo(() => {
83
- const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
159
+ let instructions = selectedDocumentType
160
+ ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
161
+ : getLocalizedText(component.instructions);
162
+ if (scanPhase === 'barcode') {
163
+ instructions = '';
164
+ }
84
165
  const isLandscape = screenWidth > screenHeight;
85
166
  let xMin, xMax, yMin, yMax;
86
167
  if (Platform.OS === 'web') {
@@ -101,21 +182,36 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
101
182
  yMin = 34;
102
183
  yMax = 66;
103
184
  }
104
- const platformBbox = { xMin, yMin, xMax, yMax, borderColor: '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
185
+ const platformBbox = {
186
+ xMin,
187
+ yMin,
188
+ xMax,
189
+ yMax,
190
+ borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60',
191
+ borderWidth: Platform.OS === 'web' ? 2 : 3,
192
+ cornerRadius: 8
193
+ };
105
194
  return {
106
195
  cameraType: Platform.OS === 'web' ? cameraType : 'back',
107
196
  flashMode: 'auto',
108
197
  overlay: { guideText: instructions, bbox: platformBbox }
109
198
  };
110
- }, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
199
+ }, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight, scanPhase]);
111
200
  const retakePicture = (sideToRetake) => {
112
201
  setIsProcessingCapture(false);
113
202
  setProcessingImagePath(null);
203
+ scannedBarcodeRef.current = null;
204
+ setScanPhase('image');
205
+ setPendingCAImage(null);
114
206
  setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
115
207
  setShowCamera(true);
116
208
  refreshCamera();
117
209
  actions.showCustomStepper(false);
118
- setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
210
+ setCapturedImages((prev) => {
211
+ const newState = { ...prev };
212
+ delete newState[sideToRetake];
213
+ return newState;
214
+ });
119
215
  if (value) {
120
216
  const newValue = { ...value };
121
217
  delete newValue[sideToRetake];
@@ -124,17 +220,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
124
220
  };
125
221
  const getCurrentSideVerification = (currentSide, countryKey) => {
126
222
  const rawDocType = countrySelectionData?.documentType;
127
- if (!rawDocType || !countryKey) {
223
+ if (!rawDocType || !countryKey)
128
224
  return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
129
- }
130
225
  const jsonDocMap = {
131
- 'nationalId': 'identity_card',
132
- 'national_id': 'identity_card',
133
- 'passport': 'passport',
134
- 'driversLicense': 'drivers_licence',
135
- 'drivers_licence': 'drivers_licence',
136
- 'residencePermit': 'permanent_residence',
137
- 'healthInsuranceCard': 'health_insurance_card'
226
+ 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
227
+ 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
228
+ 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
138
229
  };
139
230
  const searchDocType = jsonDocMap[rawDocType] || rawDocType;
140
231
  const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
@@ -145,24 +236,19 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
145
236
  if (foundKey)
146
237
  countryMapping = baseMapping[foundKey];
147
238
  }
148
- if (!countryMapping) {
149
- return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
150
- }
151
- // Use the translated name to look up the document in the JSON
152
- const regionMapping = countryMapping[searchDocType];
153
- if (!regionMapping) {
239
+ if (!countryMapping || !countryMapping[searchDocType])
154
240
  return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
155
- }
156
241
  const authMethod = [];
157
242
  const mrzTypes = [];
158
- const key = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
243
+ const rawRegionKey = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
244
+ const key = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
245
+ const regionMapping = countryMapping[searchDocType];
159
246
  if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
160
247
  regionMapping[key].forEach((item) => {
161
- if (item[currentSide]) {
248
+ if (item[currentSide])
162
249
  authMethod.push(item[currentSide]);
163
- if (item?.mrz_type)
164
- mrzTypes.push(item?.mrz_type);
165
- }
250
+ if (item?.mrz_type)
251
+ mrzTypes.push(item?.mrz_type);
166
252
  });
167
253
  }
168
254
  return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
@@ -196,8 +282,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
196
282
  let imagePathForUpload = capturePath;
197
283
  const overlayBbox = cameraConfig.overlay.bbox;
198
284
  const uiCropBbox = {
199
- minX: overlayBbox.xMin / 100,
200
- minY: overlayBbox.yMin / 100,
285
+ minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
201
286
  width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
202
287
  height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
203
288
  };
@@ -215,10 +300,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
215
300
  };
216
301
  setCapturedImages(newImages);
217
302
  if (verified.country && verified.documentType) {
303
+ const docType = verified.documentType;
304
+ const resolvedDocumentType = GovernmentDocumentTypeBackend[docType] || (Object.values(GovernmentDocumentTypeBackend).includes(docType) ? docType : null) || Object.entries(GovernmentDocumentTypeBackend).find(([, v]) => v === docType)?.[1] || docType;
218
305
  onValueChange({
219
306
  ...newImages,
220
307
  country: verified.country,
221
- documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
308
+ documentType: resolvedDocumentType,
222
309
  ...(verified.region ? { region: verified.region } : {})
223
310
  });
224
311
  }
@@ -228,6 +315,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
228
315
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
229
316
  setIsProcessingCapture(false);
230
317
  setProcessingImagePath(null);
318
+ setScanPhase('image');
319
+ setPendingCAImage(null);
231
320
  }, 600);
232
321
  }
233
322
  catch (e) {
@@ -239,8 +328,90 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
239
328
  setProcessingImagePath(null);
240
329
  }
241
330
  };
331
+ const isBarcodeRequired = () => {
332
+ if (currentSide !== 'back')
333
+ return false;
334
+ if (!countryData?.code || !selectedDocumentType?.type)
335
+ return false;
336
+ const rawDocType = selectedDocumentType.type;
337
+ const jsonDocMap = {
338
+ 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
339
+ 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
340
+ 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
341
+ };
342
+ const searchDocType = jsonDocMap[rawDocType] || rawDocType;
343
+ const rawCountryName = ISO_TO_COUNTRY_NAME[countryData.code] || countryData.code;
344
+ const baseMapping = REGION_MAPPING.regionMapping || REGION_MAPPING;
345
+ let countryMapping = baseMapping[rawCountryName];
346
+ if (!countryMapping) {
347
+ const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryData.code.toLowerCase());
348
+ if (foundKey)
349
+ countryMapping = baseMapping[foundKey];
350
+ }
351
+ if (!countryMapping || !countryMapping[searchDocType])
352
+ return false;
353
+ const rawRegionKey = countryData.region?.trim()?.length > 0 ? countryData.region.trim() : 'root';
354
+ const regionKey = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
355
+ const regionData = countryMapping[searchDocType][regionKey];
356
+ if (!regionData || !Array.isArray(regionData))
357
+ return false;
358
+ const requiresBarcode = regionData.some((item) => {
359
+ const method = String(item['back'] || '').toUpperCase();
360
+ return method.includes('BARCODE') || method.includes('2D');
361
+ });
362
+ logger.log(`[Barcode Check] Dynamic check for ${countryData.code} - ${searchDocType} - ${regionKey}: requiresBarcode=${requiresBarcode}`);
363
+ return requiresBarcode;
364
+ };
365
+ const processCanadianBarcode = async (barcodeString) => {
366
+ if (isProcessingCapture || !pendingCAImage)
367
+ return;
368
+ logger.log('[CA Barcode] Valid barcode decoded. Sending to parser...');
369
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
370
+ try {
371
+ const isHIC = selectedDocumentType.type === 'health_insurance_card';
372
+ let parsedDictionary;
373
+ if (isHIC && barcodeString.length < 50) {
374
+ parsedDictionary = { data: { card_number: barcodeString, barcode_type: "1D" } };
375
+ }
376
+ else {
377
+ const token = await authentification();
378
+ parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeString, token }, env);
379
+ }
380
+ if (!parsedDictionary)
381
+ throw new Error("Could not parse the barcode data.");
382
+ const verifiedResult = {
383
+ path: pendingCAImage.path,
384
+ templatePath: pendingCAImage.templatePath,
385
+ bbox: pendingCAImage.bbox,
386
+ success: true,
387
+ mrz: JSON.stringify(parsedDictionary),
388
+ barcode: barcodeString,
389
+ isAnalyzing: false,
390
+ country: countryData?.code,
391
+ documentType: selectedDocumentType.type,
392
+ region: selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined,
393
+ };
394
+ setSilentCaptureResult(verifiedResult);
395
+ if (pendingCAImage.bbox) {
396
+ const safeBbox = pendingCAImage.bbox;
397
+ setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
398
+ }
399
+ await autoCapture(pendingCAImage.path, verifiedResult);
400
+ }
401
+ catch (error) {
402
+ console.error("Barcode processing failed", error);
403
+ setScanPhase('image');
404
+ setPendingCAImage(null);
405
+ scannedBarcodeRef.current = null;
406
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
407
+ }
408
+ };
242
409
  const handleSilentCapture = async (result) => {
243
- if (silentCaptureResult.isAnalyzing || isProcessingCapture)
410
+ if (isProcessingCapture)
411
+ return;
412
+ if (scanPhase === 'barcode')
413
+ return;
414
+ if (silentCaptureResult.isAnalyzing)
244
415
  return;
245
416
  if (result.success && result.path) {
246
417
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
@@ -253,6 +424,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
253
424
  }
254
425
  const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
255
426
  try {
427
+ // 1. Check & Crop the Template visually first
256
428
  if (!templatePath) {
257
429
  const templateType = await checkTemplateType({
258
430
  path: result.path || '',
@@ -285,50 +457,69 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
285
457
  const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
286
458
  verificationRes = await frontVerification({
287
459
  path: result.path,
288
- regionMapping: {
289
- authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
290
- mrzTypes: mrzType ? [mrzType] : []
291
- },
460
+ regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
292
461
  selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
293
- code: countryData?.code || '',
294
- currentSide,
295
- templatePath,
296
- mrzType,
297
- issuingSubdivision: issuingSubdivision
462
+ code: countryData?.code || '', currentSide, templatePath, mrzType,
463
+ issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode
298
464
  }, env);
299
465
  }
300
466
  else {
467
+ logger.log("Attempting backVerification with:", { path: result.path, templatePath, side: currentSide });
301
468
  const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
302
469
  const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
303
470
  const frontTemplatePath = capturedImages['front']?.templatePath || '';
471
+ // This validates if the back matches the front template!
304
472
  verificationRes = await backVerification({
305
473
  path: result.path,
306
- regionMapping: {
307
- authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
308
- mrzTypes: backMrzType ? [backMrzType] : []
309
- },
474
+ regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
310
475
  selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
311
- code: countryData?.code || '',
312
- currentSide,
313
- templatePath,
314
- mrzType: backMrzType,
315
- templateResponse,
316
- issuingSubdivision: issuingSubdivision,
317
- frontTemplatePath: frontTemplatePath
476
+ code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse,
477
+ issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath
318
478
  }, env);
319
479
  }
320
480
  const bbox = verificationRes?.bbox || templateBbox;
321
- const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
481
+ const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
482
+ if (isBarcodeRequired()) {
483
+ const barcodeData = scannedBarcodeRef.current;
484
+ const docTypeStr = selectedDocumentType.type;
485
+ const isHIC = docTypeStr === 'health_insurance_card';
486
+ const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
487
+ // If the barcode was caught extremely fast in the same frame
488
+ if (isValidScan) {
489
+ logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
490
+ let parsedDictionary;
491
+ if (isHIC && barcodeData.length < 50) {
492
+ parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
493
+ }
494
+ else {
495
+ const token = await authentification();
496
+ parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
497
+ }
498
+ const verifiedResult = {
499
+ path: result.path, templatePath: templatePath, bbox: bbox, success: true,
500
+ mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
501
+ country: countryData?.code, documentType: selectedDocumentType.type,
502
+ region: selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined,
503
+ };
504
+ setSilentCaptureResult(verifiedResult);
505
+ if (bbox) {
506
+ const safeBbox = bbox;
507
+ setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
508
+ }
509
+ await autoCapture(result.path, verifiedResult);
510
+ return;
511
+ }
512
+ // If validation passed but we still need the barcode, trigger the UI phase!
513
+ logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
514
+ setPendingCAImage({ path: result.path, templateResponse, templatePath, bbox });
515
+ setScanPhase('barcode');
516
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
517
+ return;
518
+ }
519
+ // 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
322
520
  const verifiedResult = {
323
- path: result.path,
324
- templatePath,
325
- bbox,
326
- success: true,
327
- mrz,
328
- isAnalyzing: false,
329
- country: countryData?.code,
330
- documentType: selectedDocumentType.type,
331
- region: issuingSubdivision
521
+ path: result.path, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
522
+ country: countryData?.code, documentType: selectedDocumentType.type, region: issuingSubdivision
332
523
  };
333
524
  setSilentCaptureResult(verifiedResult);
334
525
  if (bbox)
@@ -351,10 +542,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
351
542
  else if (rawMessage.includes('ne correspond pas au recto')) {
352
543
  userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
353
544
  }
545
+ // The error is thrown here, completely blocking the switch to 'barcode' phase
354
546
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
355
547
  }
356
548
  }
357
549
  };
550
+ useEffect(() => {
551
+ if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
552
+ const bData = silentCaptureResult.barcode;
553
+ if (typeof bData === 'string' && bData.length > 50) {
554
+ logger.log('[CA Barcode] Safety Net Triggered: Processing barcode.');
555
+ processCanadianBarcode(bData);
556
+ }
557
+ }
558
+ }, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
358
559
  const handleError = (event) => {
359
560
  showAlert('Erreur', event.message);
360
561
  setShowCamera(false);
@@ -363,6 +564,36 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
363
564
  useEffect(() => {
364
565
  actions.showCustomStepper(!showCamera);
365
566
  }, [showCamera]);
567
+ useEffect(() => {
568
+ if (Platform.OS === 'web' && typeof window !== 'undefined') {
569
+ try {
570
+ const url = new URL(window.location.origin);
571
+ if (url.hostname === 'localhost') {
572
+ url.hostname = '192.168.0.33';
573
+ }
574
+ const params = new URLSearchParams(window.location.search);
575
+ params.set('session_id', state.session.session_id);
576
+ params.set('resume', 'true');
577
+ if (state.currentComponentIndex !== undefined) {
578
+ params.set('component_index', String(state.currentComponentIndex));
579
+ }
580
+ if (countryData?.code) {
581
+ params.set('country', countryData.code);
582
+ }
583
+ if (selectedDocumentType?.type) {
584
+ params.set('document_type', selectedDocumentType.type);
585
+ }
586
+ if (selectedDocumentType?.region && selectedDocumentType.region !== 'root') {
587
+ params.set('region', selectedDocumentType.region);
588
+ }
589
+ url.hash = params.toString();
590
+ setCurrentUrl(url.toString());
591
+ }
592
+ catch (e) {
593
+ console.error("Failed to construct QR handoff URL", e);
594
+ }
595
+ }
596
+ }, [state.session.session_id, state.currentComponentIndex, countryData, selectedDocumentType]);
366
597
  if (!countrySelectionData || !selectedDocumentType) {
367
598
  return (<View style={styles.root}>
368
599
  <View style={styles.previewContainer}>
@@ -393,25 +624,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
393
624
  </Text>
394
625
  </View>
395
626
  </View>
396
-
627
+
397
628
  <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
398
629
  {Platform.OS === 'web' && (<View style={styles.webTopControls}>
399
630
  <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
400
631
  <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
401
632
  </TouchableOpacity>
402
633
  </View>)}
403
-
404
- <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={<>
405
- <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={{
634
+
635
+ <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} isScanningBarcode={currentSide === 'back'} barcodeTypes={targetBarcodeTypes} onBarcodeScanned={(barcodeString) => {
636
+ const docTypeStr = selectedDocumentType?.type;
637
+ const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
638
+ if (isFullDataDoc && barcodeString.length < 50)
639
+ return;
640
+ if (scannedBarcodeRef.current !== barcodeString) {
641
+ scannedBarcodeRef.current = barcodeString;
642
+ if (scanPhase === 'barcode' && pendingCAImage) {
643
+ processCanadianBarcode(barcodeString);
644
+ }
645
+ else {
646
+ setSilentCaptureResult(prev => ({ ...prev, barcode: barcodeString }));
647
+ }
648
+ }
649
+ }} overlayComponent={<>
650
+ <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 || scanPhase === 'barcode'} language={state.currentLanguage} stepperProps={{
406
651
  back: () => {
407
652
  if (currentSide === 'back') {
653
+ scannedBarcodeRef.current = null;
654
+ setScanPhase('image');
655
+ setPendingCAImage(null);
408
656
  setCurrentSide('front');
409
657
  setShowCamera(false);
410
658
  setIsProcessingCapture(false);
411
659
  setProcessingImagePath(null);
412
660
  if (capturedImages['front']?.dir) {
413
661
  const frontImage = capturedImages['front'];
414
- setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
662
+ setSilentCaptureResult((prev) => ({
663
+ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front']
664
+ }));
415
665
  }
416
666
  else {
417
667
  setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
@@ -422,10 +672,28 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
422
672
  }
423
673
  },
424
674
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
425
- step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
675
+ step: state.currentComponentIndex + 1,
676
+ totalSteps: state.template.components.length,
677
+ side: currentSide,
426
678
  }}/>
427
679
  </>}/>
428
-
680
+
681
+ {scanPhase === 'barcode' && !isBusy && (<View style={styles.barcodePromptContainer}>
682
+ <View style={styles.barcodePromptContent}>
683
+ <View style={styles.barcodeIconContainer}>
684
+ <Text style={styles.barcodeIcon}>✓</Text>
685
+ </View>
686
+ <View style={styles.barcodeTextContainer}>
687
+ <Text style={styles.barcodePromptTitle}>
688
+ {state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}
689
+ </Text>
690
+ <Text style={styles.barcodePromptDesc}>
691
+ {state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}
692
+ </Text>
693
+ </View>
694
+ </View>
695
+ </View>)}
696
+
429
697
  {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
430
698
  <View style={styles.topAnalyzingPill}>
431
699
  <ActivityIndicator size="small" color="white"/>
@@ -434,7 +702,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
434
702
  </Text>
435
703
  </View>
436
704
  </View>)}
437
-
705
+
438
706
  {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
439
707
  {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
440
708
  <View style={styles.processingOverlay}>
@@ -444,7 +712,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
444
712
  </Text>
445
713
  </View>
446
714
  </View>)}
447
-
715
+
448
716
  {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
449
717
  {silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
450
718
  <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
@@ -465,7 +733,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
465
733
  <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
466
734
  </TouchableOpacity>
467
735
  </View>) : null}
468
-
736
+
469
737
  {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
470
738
  <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
471
739
  </View>) : null}
@@ -483,14 +751,41 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
483
751
  <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
484
752
  {getLocalizedText(component.instructions)}
485
753
  </Text>
754
+
486
755
  <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
487
756
  <View style={styles.imagePreviewWrapper}>
488
757
  {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
489
758
  </View>
759
+
490
760
  {!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
491
- <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
492
- {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/>)}
761
+
762
+ {isWebScanningRestricted ? (<>
763
+ <View style={styles.restrictionWarningBanner}>
764
+ <Text style={styles.restrictionWarningTitle}>
765
+ {state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}
766
+ </Text>
767
+ <Text style={styles.restrictionWarningText}>
768
+ {state.currentLanguage === 'en'
769
+ ? `This document can only be scanned using our mobile app.`
770
+ : `Ce document ne peut être scanné qu'avec notre application mobile.`}
771
+ </Text>
772
+ </View>
773
+
774
+ <Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="primary" size="large" fullWidth/>
775
+ </>) : (
776
+ /* 🟢 NORMAL FLOW: Show "Start Scanning" and optional web handoff */
777
+ <>
778
+ <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => {
779
+ setShowCamera(true);
780
+ refreshCamera();
781
+ actions.showCustomStepper(false);
782
+ }} variant="primary" size="large" fullWidth/>
783
+
784
+ {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/>)}
785
+ </>)}
786
+
493
787
  </View>)}
788
+
494
789
  {capturedImages[currentSide]?.dir && (<>
495
790
  <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
496
791
  <Button title={t('common.next')} onPress={() => {
@@ -508,6 +803,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
508
803
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
509
804
  setIsProcessingCapture(false);
510
805
  setProcessingImagePath(null);
806
+ setScanPhase('image');
807
+ setPendingCAImage(null);
511
808
  }
512
809
  }} variant="primary" size="large" fullWidth/>
513
810
  </>)}
@@ -515,20 +812,64 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
515
812
  </View>
516
813
  </ScrollView>
517
814
  </View>
815
+
518
816
  {showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
519
- <View style={styles.qrModalContainer}>
520
- <Text style={styles.qrModalTitle}>
521
- {state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
522
- </Text>
523
- <Text style={styles.qrModalText}>
524
- {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."}
525
- </Text>
526
- <View style={styles.qrCodeWrapper}>
527
- <QRCode value={currentUrl} size={220} backgroundColor="transparent"/>
817
+ <View style={styles.qrModalContainer}>
818
+ <View style={styles.qrModalHeader}>
819
+ <Text style={styles.qrModalTitle}>
820
+ {state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
821
+ </Text>
822
+ <Text style={styles.qrModalText}>
823
+ {state.currentLanguage === 'en'
824
+ ? "Scan the QR code to download our mobile app and complete the scanning process securely."
825
+ : "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
826
+ </Text>
827
+ </View>
828
+
829
+ <View style={styles.qrToggleContainer}>
830
+ <TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('android')}>
831
+ <Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
832
+ Android
833
+ </Text>
834
+ </TouchableOpacity>
835
+ <TouchableOpacity style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]} onPress={() => setQrPlatform('ios')}>
836
+ <Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
837
+ iOS (Apple)
838
+ </Text>
839
+ </TouchableOpacity>
840
+ </View>
841
+
842
+ <View style={styles.qrCodeWrapper}>
843
+ <QRCode value={qrPlatform === 'android'
844
+ ? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
845
+ : 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'} size={200} // Slightly reduced to accommodate the toggle switch
846
+ backgroundColor="#FFFFFF" color="#0F172A"/>
847
+ </View>
848
+
849
+ {isWebScanningRestricted && (<View style={styles.storeLinksContainer}>
850
+ <Text style={styles.storeLinksTitle}>
851
+ {state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
852
+ </Text>
853
+ <View style={styles.storeButtonsRow}>
854
+ <TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')}>
855
+ <Text style={styles.storeButtonText}>Google Play</Text>
856
+ </TouchableOpacity>
857
+ <TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')}>
858
+ <Text style={styles.storeButtonText}>App Store</Text>
859
+ </TouchableOpacity>
860
+ </View>
861
+ </View>)}
862
+
863
+ <View style={styles.listeningContainer}>
864
+ <ActivityIndicator size="small" color="#2DBD60"/>
865
+ <Text style={styles.listeningText}>
866
+ {state.currentLanguage === 'en' ? "Waiting for you to complete on mobile..." : "En attente de votre complétion sur mobile..."}
867
+ </Text>
868
+ </View>
869
+
870
+ <Button title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
528
871
  </View>
529
- <Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
530
- </View>
531
- </View>)}
872
+ </View>)}
532
873
  </View>);
533
874
  };
534
875
  const styles = StyleSheet.create({
@@ -567,10 +908,83 @@ const styles = StyleSheet.create({
567
908
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
568
909
  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 },
569
910
  analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
570
- qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
571
- 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, },
572
- qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
573
- qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
574
- qrCodeWrapper: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#E2E8F0', marginBottom: 24, }
911
+ qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(15, 23, 42, 0.85)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(8px)' } }) },
912
+ qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth: 420, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 40, elevation: 24 },
913
+ qrModalHeader: { alignItems: 'center', marginBottom: 24 },
914
+ qrModalTitle: { fontSize: 24, fontWeight: '800', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
915
+ qrModalText: { fontSize: 15, color: '#475569', textAlign: 'center', lineHeight: 22 },
916
+ qrCodeWrapper: { padding: 20, backgroundColor: '#FFFFFF', borderRadius: 20, borderWidth: 2, borderColor: '#E2E8F0', marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.05, shadowRadius: 10 },
917
+ listeningContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F0FDF4', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 12, marginBottom: 24, borderWidth: 1, borderColor: '#BBF7D0' },
918
+ listeningText: { marginLeft: 12, color: '#166534', fontSize: 14, fontWeight: '600' },
919
+ barcodePromptContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 150, paddingHorizontal: 24, },
920
+ barcodePromptContent: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(15, 23, 42, 0.85)', paddingVertical: 14, paddingHorizontal: 20, borderRadius: 16, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.15)', shadowColor: '#000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 12, elevation: 10, maxWidth: 400, ...Platform.select({ web: { backdropFilter: 'blur(8px)' }, ios: { backdropFilter: 'blur(8px)' }, }), },
921
+ barcodeIconContainer: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(45, 189, 96, 0.2)', justifyContent: 'center', alignItems: 'center', marginRight: 14, borderWidth: 1, borderColor: 'rgba(45, 189, 96, 0.5)', },
922
+ barcodeIcon: { fontSize: 16, color: '#2DBD60', fontWeight: 'bold', },
923
+ barcodeTextContainer: { flex: 1, },
924
+ barcodePromptTitle: { color: '#2DBD60', fontSize: 16, fontWeight: '700', marginBottom: 4, },
925
+ barcodePromptDesc: { color: '#F8FAFC', fontSize: 14, fontWeight: '500', lineHeight: 20, },
926
+ storeLinksContainer: { width: '100%', alignItems: 'center', marginBottom: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E2E8F0' },
927
+ storeLinksTitle: { fontSize: 14, color: '#64748B', fontWeight: '600', marginBottom: 12 },
928
+ storeButtonsRow: { flexDirection: 'row', gap: 12, width: '100%', justifyContent: 'center' },
929
+ storeButton: { flex: 1, backgroundColor: '#0F172A', paddingVertical: 12, borderRadius: 10, alignItems: 'center' },
930
+ storeButtonText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 14 },
931
+ restrictionWarningBanner: {
932
+ backgroundColor: '#FFFBEB', // Light amber background
933
+ borderWidth: 1,
934
+ borderColor: '#F59E0B', // Amber border
935
+ borderRadius: 12,
936
+ padding: 16,
937
+ marginBottom: 4,
938
+ width: '100%',
939
+ shadowColor: '#F59E0B',
940
+ shadowOffset: { width: 0, height: 2 },
941
+ shadowOpacity: 0.1,
942
+ shadowRadius: 4,
943
+ },
944
+ restrictionWarningTitle: {
945
+ color: '#B45309', // Dark amber text
946
+ fontSize: 15,
947
+ fontWeight: '800',
948
+ marginBottom: 6,
949
+ textAlign: 'center'
950
+ },
951
+ restrictionWarningText: {
952
+ color: '#D97706',
953
+ fontSize: 13,
954
+ fontWeight: '500',
955
+ textAlign: 'center',
956
+ lineHeight: 18
957
+ },
958
+ qrToggleContainer: {
959
+ flexDirection: 'row',
960
+ backgroundColor: '#F1F5F9', // Light gray background
961
+ borderRadius: 12,
962
+ padding: 4,
963
+ marginBottom: 20,
964
+ width: '100%',
965
+ },
966
+ qrToggleButton: {
967
+ flex: 1,
968
+ paddingVertical: 10,
969
+ borderRadius: 8,
970
+ alignItems: 'center',
971
+ justifyContent: 'center',
972
+ },
973
+ qrToggleButtonActive: {
974
+ backgroundColor: '#FFFFFF',
975
+ shadowColor: '#0F172A',
976
+ shadowOffset: { width: 0, height: 2 },
977
+ shadowOpacity: 0.08,
978
+ shadowRadius: 4,
979
+ elevation: 2,
980
+ },
981
+ qrToggleText: {
982
+ fontSize: 14,
983
+ fontWeight: '600',
984
+ color: '#64748B', // Inactive text color
985
+ },
986
+ qrToggleTextActive: {
987
+ color: '#0F172A', // Active text color
988
+ },
575
989
  });
576
990
  //# sourceMappingURL=IDCardCapture.js.map