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