@sanctum-key/react-native-sdk 1.0.23 → 1.0.25

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 (76) hide show
  1. package/build/package.json +3 -1
  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 +119 -26
  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 +512 -87
  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.js +14 -14
  34. package/build/src/config/region_mapping.js.map +1 -1
  35. package/build/src/config/region_mapping.json +0 -14
  36. package/build/src/modules/api/CardAuthentification.d.ts +2 -0
  37. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  38. package/build/src/modules/api/CardAuthentification.js +23 -40
  39. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  40. package/build/src/modules/api/KYCService.d.ts +7 -2
  41. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  42. package/build/src/modules/api/KYCService.js +163 -57
  43. package/build/src/modules/api/KYCService.js.map +1 -1
  44. package/build/src/types/KYC.types.d.ts +1 -0
  45. package/build/src/types/KYC.types.d.ts.map +1 -1
  46. package/build/src/types/KYC.types.js.map +1 -1
  47. package/build/src/utils/aamvaParser.d.ts +62 -0
  48. package/build/src/utils/aamvaParser.d.ts.map +1 -0
  49. package/build/src/utils/aamvaParser.js +91 -0
  50. package/build/src/utils/aamvaParser.js.map +1 -0
  51. package/build/src/utils/cropByObb.d.ts.map +1 -1
  52. package/build/src/utils/cropByObb.js +4 -3
  53. package/build/src/utils/cropByObb.js.map +1 -1
  54. package/build/src/web/WebKYCEntry.d.ts +1 -1
  55. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  56. package/build/src/web/WebKYCEntry.js +105 -239
  57. package/build/src/web/WebKYCEntry.js.map +1 -1
  58. package/package.json +3 -1
  59. package/src/components/EnhancedCameraView.tsx +116 -42
  60. package/src/components/EnhancedCameraView.web.tsx +98 -338
  61. package/src/components/KYCElements/CameraCapture.tsx +1 -1
  62. package/src/components/KYCElements/CountrySelection.tsx +60 -240
  63. package/src/components/KYCElements/CountrySelectionTemplate.tsx +148 -29
  64. package/src/components/KYCElements/IDCardCapture.tsx +675 -142
  65. package/src/components/KYCElements/VerificationProgressTemplate.tsx +112 -65
  66. package/src/components/OverLay/type.ts +1 -1
  67. package/src/components/WebBarcodeScanner.tsx +273 -0
  68. package/src/config/KYCConfigs.ts +2 -2
  69. package/src/config/region_mapping.json +0 -14
  70. package/src/config/region_mapping.ts +14 -14
  71. package/src/modules/api/CardAuthentification.ts +44 -58
  72. package/src/modules/api/KYCService.ts +236 -82
  73. package/src/types/KYC.types.ts +1 -0
  74. package/src/utils/aamvaParser.ts +102 -0
  75. package/src/utils/cropByObb.ts +5 -3
  76. 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,9 +220,14 @@ 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
- }
225
+ const jsonDocMap = {
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'
229
+ };
230
+ const searchDocType = jsonDocMap[rawDocType] || rawDocType;
130
231
  const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
131
232
  const baseMapping = REGION_MAPPING.regionMapping || REGION_MAPPING;
132
233
  let countryMapping = baseMapping[rawCountryName];
@@ -135,23 +236,19 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
135
236
  if (foundKey)
136
237
  countryMapping = baseMapping[foundKey];
137
238
  }
138
- if (!countryMapping) {
139
- return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
140
- }
141
- const regionMapping = countryMapping[rawDocType];
142
- if (!regionMapping) {
239
+ if (!countryMapping || !countryMapping[searchDocType])
143
240
  return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
144
- }
145
241
  const authMethod = [];
146
242
  const mrzTypes = [];
147
- 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];
148
246
  if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
149
247
  regionMapping[key].forEach((item) => {
150
- if (item[currentSide]) {
248
+ if (item[currentSide])
151
249
  authMethod.push(item[currentSide]);
152
- if (item?.mrz_type)
153
- mrzTypes.push(item?.mrz_type);
154
- }
250
+ if (item?.mrz_type)
251
+ mrzTypes.push(item?.mrz_type);
155
252
  });
156
253
  }
157
254
  return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
@@ -185,8 +282,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
185
282
  let imagePathForUpload = capturePath;
186
283
  const overlayBbox = cameraConfig.overlay.bbox;
187
284
  const uiCropBbox = {
188
- minX: overlayBbox.xMin / 100,
189
- minY: overlayBbox.yMin / 100,
285
+ minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
190
286
  width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
191
287
  height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
192
288
  };
@@ -204,10 +300,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
204
300
  };
205
301
  setCapturedImages(newImages);
206
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;
207
305
  onValueChange({
208
306
  ...newImages,
209
307
  country: verified.country,
210
- documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
308
+ documentType: resolvedDocumentType,
211
309
  ...(verified.region ? { region: verified.region } : {})
212
310
  });
213
311
  }
@@ -217,6 +315,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
217
315
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
218
316
  setIsProcessingCapture(false);
219
317
  setProcessingImagePath(null);
318
+ setScanPhase('image');
319
+ setPendingCAImage(null);
220
320
  }, 600);
221
321
  }
222
322
  catch (e) {
@@ -228,8 +328,90 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
228
328
  setProcessingImagePath(null);
229
329
  }
230
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
+ };
231
409
  const handleSilentCapture = async (result) => {
232
- if (silentCaptureResult.isAnalyzing || isProcessingCapture)
410
+ if (isProcessingCapture)
411
+ return;
412
+ if (scanPhase === 'barcode')
413
+ return;
414
+ if (silentCaptureResult.isAnalyzing)
233
415
  return;
234
416
  if (result.success && result.path) {
235
417
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
@@ -242,6 +424,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
242
424
  }
243
425
  const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
244
426
  try {
427
+ // 1. Check & Crop the Template visually first
245
428
  if (!templatePath) {
246
429
  const templateType = await checkTemplateType({
247
430
  path: result.path || '',
@@ -274,50 +457,69 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
274
457
  const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
275
458
  verificationRes = await frontVerification({
276
459
  path: result.path,
277
- regionMapping: {
278
- authMethod: matchedAuthMethod ? [matchedAuthMethod] : [],
279
- mrzTypes: mrzType ? [mrzType] : []
280
- },
460
+ regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
281
461
  selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
282
- code: countryData?.code || '',
283
- currentSide,
284
- templatePath,
285
- mrzType,
286
- issuingSubdivision: issuingSubdivision
462
+ code: countryData?.code || '', currentSide, templatePath, mrzType,
463
+ issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode
287
464
  }, env);
288
465
  }
289
466
  else {
467
+ logger.log("Attempting backVerification with:", { path: result.path, templatePath, side: currentSide });
290
468
  const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
291
469
  const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
292
470
  const frontTemplatePath = capturedImages['front']?.templatePath || '';
471
+ // This validates if the back matches the front template!
293
472
  verificationRes = await backVerification({
294
473
  path: result.path,
295
- regionMapping: {
296
- authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [],
297
- mrzTypes: backMrzType ? [backMrzType] : []
298
- },
474
+ regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
299
475
  selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
300
- code: countryData?.code || '',
301
- currentSide,
302
- templatePath,
303
- mrzType: backMrzType,
304
- templateResponse,
305
- issuingSubdivision: issuingSubdivision,
306
- frontTemplatePath: frontTemplatePath
476
+ code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse,
477
+ issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath
307
478
  }, env);
308
479
  }
309
480
  const bbox = verificationRes?.bbox || templateBbox;
310
- 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)
311
520
  const verifiedResult = {
312
- path: result.path,
313
- templatePath,
314
- bbox,
315
- success: true,
316
- mrz,
317
- isAnalyzing: false,
318
- country: countryData?.code,
319
- documentType: selectedDocumentType.type,
320
- region: issuingSubdivision
521
+ path: result.path, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
522
+ country: countryData?.code, documentType: selectedDocumentType.type, region: issuingSubdivision
321
523
  };
322
524
  setSilentCaptureResult(verifiedResult);
323
525
  if (bbox)
@@ -340,10 +542,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
340
542
  else if (rawMessage.includes('ne correspond pas au recto')) {
341
543
  userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
342
544
  }
545
+ // The error is thrown here, completely blocking the switch to 'barcode' phase
343
546
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
344
547
  }
345
548
  }
346
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]);
347
559
  const handleError = (event) => {
348
560
  showAlert('Erreur', event.message);
349
561
  setShowCamera(false);
@@ -352,6 +564,36 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
352
564
  useEffect(() => {
353
565
  actions.showCustomStepper(!showCamera);
354
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]);
355
597
  if (!countrySelectionData || !selectedDocumentType) {
356
598
  return (<View style={styles.root}>
357
599
  <View style={styles.previewContainer}>
@@ -382,25 +624,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
382
624
  </Text>
383
625
  </View>
384
626
  </View>
385
-
627
+
386
628
  <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
387
629
  {Platform.OS === 'web' && (<View style={styles.webTopControls}>
388
630
  <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
389
631
  <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
390
632
  </TouchableOpacity>
391
633
  </View>)}
392
-
393
- <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={<>
394
- <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={{
395
651
  back: () => {
396
652
  if (currentSide === 'back') {
653
+ scannedBarcodeRef.current = null;
654
+ setScanPhase('image');
655
+ setPendingCAImage(null);
397
656
  setCurrentSide('front');
398
657
  setShowCamera(false);
399
658
  setIsProcessingCapture(false);
400
659
  setProcessingImagePath(null);
401
660
  if (capturedImages['front']?.dir) {
402
661
  const frontImage = capturedImages['front'];
403
- 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
+ }));
404
665
  }
405
666
  else {
406
667
  setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
@@ -411,10 +672,28 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
411
672
  }
412
673
  },
413
674
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
414
- 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,
415
678
  }}/>
416
679
  </>}/>
417
-
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
+
418
697
  {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
419
698
  <View style={styles.topAnalyzingPill}>
420
699
  <ActivityIndicator size="small" color="white"/>
@@ -423,7 +702,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
423
702
  </Text>
424
703
  </View>
425
704
  </View>)}
426
-
705
+
427
706
  {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
428
707
  {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
429
708
  <View style={styles.processingOverlay}>
@@ -433,7 +712,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
433
712
  </Text>
434
713
  </View>
435
714
  </View>)}
436
-
715
+
437
716
  {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
438
717
  {silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
439
718
  <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
@@ -454,7 +733,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
454
733
  <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
455
734
  </TouchableOpacity>
456
735
  </View>) : null}
457
-
736
+
458
737
  {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
459
738
  <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
460
739
  </View>) : null}
@@ -472,14 +751,41 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
472
751
  <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
473
752
  {getLocalizedText(component.instructions)}
474
753
  </Text>
754
+
475
755
  <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
476
756
  <View style={styles.imagePreviewWrapper}>
477
757
  {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
478
758
  </View>
759
+
479
760
  {!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
480
- <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
481
- {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
+
482
787
  </View>)}
788
+
483
789
  {capturedImages[currentSide]?.dir && (<>
484
790
  <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
485
791
  <Button title={t('common.next')} onPress={() => {
@@ -497,6 +803,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
497
803
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
498
804
  setIsProcessingCapture(false);
499
805
  setProcessingImagePath(null);
806
+ setScanPhase('image');
807
+ setPendingCAImage(null);
500
808
  }
501
809
  }} variant="primary" size="large" fullWidth/>
502
810
  </>)}
@@ -504,20 +812,64 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
504
812
  </View>
505
813
  </ScrollView>
506
814
  </View>
815
+
507
816
  {showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
508
- <View style={styles.qrModalContainer}>
509
- <Text style={styles.qrModalTitle}>
510
- {state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
511
- </Text>
512
- <Text style={styles.qrModalText}>
513
- {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."}
514
- </Text>
515
- <View style={styles.qrCodeWrapper}>
516
- <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/>
517
871
  </View>
518
- <Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
519
- </View>
520
- </View>)}
872
+ </View>)}
521
873
  </View>);
522
874
  };
523
875
  const styles = StyleSheet.create({
@@ -556,10 +908,83 @@ const styles = StyleSheet.create({
556
908
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
557
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 },
558
910
  analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
559
- qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
560
- 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, },
561
- qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
562
- qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
563
- 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
+ },
564
989
  });
565
990
  //# sourceMappingURL=IDCardCapture.js.map