@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.
- package/build/package.json +3 -1
- package/build/src/components/EnhancedCameraView.d.ts +7 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +88 -30
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/EnhancedCameraView.web.d.ts +5 -1
- package/build/src/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.web.js +80 -295
- package/build/src/components/EnhancedCameraView.web.js.map +1 -1
- package/build/src/components/KYCElements/CameraCapture.js +1 -1
- package/build/src/components/KYCElements/CameraCapture.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.js +38 -218
- package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +119 -26
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +512 -87
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +93 -51
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/components/OverLay/type.d.ts +1 -0
- package/build/src/components/OverLay/type.d.ts.map +1 -1
- package/build/src/components/OverLay/type.js.map +1 -1
- package/build/src/components/WebBarcodeScanner.d.ts +9 -0
- package/build/src/components/WebBarcodeScanner.d.ts.map +1 -0
- package/build/src/components/WebBarcodeScanner.js +152 -0
- package/build/src/components/WebBarcodeScanner.js.map +1 -0
- package/build/src/config/KYCConfigs.js +2 -2
- package/build/src/config/KYCConfigs.js.map +1 -1
- package/build/src/config/region_mapping.js +14 -14
- package/build/src/config/region_mapping.js.map +1 -1
- package/build/src/config/region_mapping.json +0 -14
- package/build/src/modules/api/CardAuthentification.d.ts +2 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +23 -40
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +7 -2
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +163 -57
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/types/KYC.types.d.ts +1 -0
- package/build/src/types/KYC.types.d.ts.map +1 -1
- package/build/src/types/KYC.types.js.map +1 -1
- package/build/src/utils/aamvaParser.d.ts +62 -0
- package/build/src/utils/aamvaParser.d.ts.map +1 -0
- package/build/src/utils/aamvaParser.js +91 -0
- package/build/src/utils/aamvaParser.js.map +1 -0
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +4 -3
- package/build/src/utils/cropByObb.js.map +1 -1
- package/build/src/web/WebKYCEntry.d.ts +1 -1
- package/build/src/web/WebKYCEntry.d.ts.map +1 -1
- package/build/src/web/WebKYCEntry.js +105 -239
- package/build/src/web/WebKYCEntry.js.map +1 -1
- package/package.json +3 -1
- package/src/components/EnhancedCameraView.tsx +116 -42
- package/src/components/EnhancedCameraView.web.tsx +98 -338
- package/src/components/KYCElements/CameraCapture.tsx +1 -1
- package/src/components/KYCElements/CountrySelection.tsx +60 -240
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +148 -29
- package/src/components/KYCElements/IDCardCapture.tsx +675 -142
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +112 -65
- package/src/components/OverLay/type.ts +1 -1
- package/src/components/WebBarcodeScanner.tsx +273 -0
- package/src/config/KYCConfigs.ts +2 -2
- package/src/config/region_mapping.json +0 -14
- package/src/config/region_mapping.ts +14 -14
- package/src/modules/api/CardAuthentification.ts +44 -58
- package/src/modules/api/KYCService.ts +236 -82
- package/src/types/KYC.types.ts +1 -0
- package/src/utils/aamvaParser.ts +102 -0
- package/src/utils/cropByObb.ts +5 -3
- 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,
|
|
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
|
-
|
|
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 = {
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
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) => {
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
394
|
-
|
|
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) => ({
|
|
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,
|
|
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
|
-
|
|
481
|
-
{
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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(
|
|
560
|
-
qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|