@sanctum-key/react-native-sdk 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +5 -2
- 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 +5 -2
- 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,13 +11,26 @@ 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
|
+
import { logger } from '../../utils/logger';
|
|
18
|
+
import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
19
|
+
import kycService, { authentification } from '../../modules/api/KYCService';
|
|
17
20
|
|
|
18
|
-
const ISO_TO_COUNTRY_NAME: Record<string, string> = {
|
|
21
|
+
const ISO_TO_COUNTRY_NAME: Record<string, string> = {
|
|
22
|
+
'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
|
|
23
|
+
'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
|
|
24
|
+
'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
interface IIDCardPayload {
|
|
28
|
+
dir: string;
|
|
29
|
+
file: string;
|
|
30
|
+
mrz: string;
|
|
31
|
+
templatePath?: string;
|
|
32
|
+
}
|
|
19
33
|
|
|
20
|
-
interface IIDCardPayload { dir: string; file: string; mrz: string; templatePath?: string; }
|
|
21
34
|
interface IDCardCaptureProps {
|
|
22
35
|
component: TemplateComponent;
|
|
23
36
|
value?: Record<string, IIDCardPayload>;
|
|
@@ -27,6 +40,14 @@ interface IDCardCaptureProps {
|
|
|
27
40
|
currentSide?: string;
|
|
28
41
|
}
|
|
29
42
|
|
|
43
|
+
const SHORT_TO_LONG_REGION: Record<string, string> = {
|
|
44
|
+
"AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba",
|
|
45
|
+
"NB": "New Brunswick", "NL": "Newfoundland and Labrador",
|
|
46
|
+
"NT": "Northwest Territories", "NS": "Nova Scotia", "NU": "Nunavut",
|
|
47
|
+
"ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
|
|
48
|
+
"SK": "Saskatchewan", "YT": "Yukon"
|
|
49
|
+
};
|
|
50
|
+
|
|
30
51
|
export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
|
|
31
52
|
const { t, locale } = useI18n();
|
|
32
53
|
const [showCamera, setShowCamera] = useState(false);
|
|
@@ -41,23 +62,42 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
41
62
|
const [cameraType, setCameraType] = useState<'back' | 'front'>('back');
|
|
42
63
|
const [showQRModal, setShowQRModal] = useState(false);
|
|
43
64
|
const [currentUrl, setCurrentUrl] = useState('');
|
|
65
|
+
const [qrPlatform, setQrPlatform] = useState<'android' | 'ios'>('android');
|
|
66
|
+
|
|
67
|
+
const [scanPhase, setScanPhase] = useState<'image' | 'barcode'>('image');
|
|
68
|
+
const [pendingCAImage, setPendingCAImage] = useState<{ path: string, templateResponse: any, templatePath: string, bbox?: IBbox } | null>(null);
|
|
69
|
+
|
|
70
|
+
const scannedBarcodeRef = useRef<string | null>(null);
|
|
44
71
|
|
|
45
72
|
useEffect(() => {
|
|
46
|
-
if (typeof window !== 'undefined' && window.location)
|
|
47
|
-
|
|
48
|
-
}
|
|
73
|
+
if (typeof window !== 'undefined' && window.location) setCurrentUrl(window.location.href);
|
|
74
|
+
console.log(currentUrl)
|
|
49
75
|
}, []);
|
|
50
76
|
|
|
51
|
-
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
77
|
+
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
78
|
+
'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
|
|
79
|
+
'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
80
|
+
'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
|
|
81
|
+
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
82
|
+
};
|
|
52
83
|
|
|
53
84
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
54
85
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
55
86
|
|
|
56
87
|
const getLocalizedText = (text: LocalizedText | Record<string, LocalizedText>): string => {
|
|
57
|
-
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
|
|
88
|
+
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
|
|
89
|
+
return text[currentSide][locale] || '';
|
|
58
90
|
return "";
|
|
59
91
|
};
|
|
60
92
|
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
95
|
+
const baseUrl = window.location.origin + window.location.pathname;
|
|
96
|
+
const handoffUrl = `${baseUrl}?session_id=${state.session.session_id}&resume=true`;
|
|
97
|
+
setCurrentUrl(handoffUrl);
|
|
98
|
+
}
|
|
99
|
+
}, [state.session.session_id]);
|
|
100
|
+
|
|
61
101
|
const countrySelectionData = useMemo(() => {
|
|
62
102
|
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
63
103
|
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
@@ -72,8 +112,54 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
72
112
|
|
|
73
113
|
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
74
114
|
|
|
115
|
+
const isWebScanningRestricted = useMemo(() => {
|
|
116
|
+
if (Platform.OS !== 'web') return false;
|
|
117
|
+
if (countryData?.code !== 'CA') return false;
|
|
118
|
+
|
|
119
|
+
const docType = selectedDocumentType?.type;
|
|
120
|
+
return docType === 'drivers_licence' ||
|
|
121
|
+
docType === 'health_insurance_card' ||
|
|
122
|
+
docType === 'national_id' ||
|
|
123
|
+
docType === 'identity_card';
|
|
124
|
+
}, [countryData?.code, selectedDocumentType?.type]);
|
|
125
|
+
|
|
126
|
+
const targetBarcodeTypes = useMemo<any[]>(() => {
|
|
127
|
+
const docType = selectedDocumentType?.type;
|
|
128
|
+
if (docType === 'health_insurance_card') return ['code-128', 'code-39', 'pdf-417'];
|
|
129
|
+
return ['pdf-417'];
|
|
130
|
+
}, [selectedDocumentType]);
|
|
131
|
+
|
|
75
132
|
const refreshCamera = () => {
|
|
76
133
|
setIsRebootingCamera(true);
|
|
134
|
+
|
|
135
|
+
if (scanPhase === 'barcode') {
|
|
136
|
+
setScanPhase('image');
|
|
137
|
+
setPendingCAImage(null);
|
|
138
|
+
scannedBarcodeRef.current = null;
|
|
139
|
+
|
|
140
|
+
setCapturedImages((prev) => {
|
|
141
|
+
const newState = { ...prev };
|
|
142
|
+
delete newState[currentSide];
|
|
143
|
+
return newState;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (value) {
|
|
147
|
+
const newValue = { ...value };
|
|
148
|
+
delete newValue[currentSide];
|
|
149
|
+
onValueChange(newValue);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setSilentCaptureResult({
|
|
153
|
+
success: false,
|
|
154
|
+
isAnalyzing: false,
|
|
155
|
+
path: '',
|
|
156
|
+
error: '',
|
|
157
|
+
templatePath: '',
|
|
158
|
+
mrz: '',
|
|
159
|
+
bbox: undefined
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
77
163
|
setTimeout(() => {
|
|
78
164
|
setCameraKey(prev => prev + 1);
|
|
79
165
|
setIsRebootingCamera(false);
|
|
@@ -91,9 +177,15 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
91
177
|
const updatedImages = value as Record<string, IIDCardPayload>;
|
|
92
178
|
setCapturedImages(updatedImages);
|
|
93
179
|
const currentImageData = updatedImages[currentSide];
|
|
180
|
+
|
|
94
181
|
if (currentImageData?.dir) {
|
|
95
182
|
setSilentCaptureResult(prev => ({
|
|
96
|
-
...prev,
|
|
183
|
+
...prev,
|
|
184
|
+
path: currentImageData.dir,
|
|
185
|
+
success: true,
|
|
186
|
+
isAnalyzing: false,
|
|
187
|
+
mrz: currentImageData.mrz || '',
|
|
188
|
+
templatePath: currentImageData.templatePath || '',
|
|
97
189
|
}));
|
|
98
190
|
}
|
|
99
191
|
}
|
|
@@ -101,7 +193,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
101
193
|
}, [value, currentSide]);
|
|
102
194
|
|
|
103
195
|
const cameraConfig = useMemo(() => {
|
|
104
|
-
|
|
196
|
+
let instructions = selectedDocumentType
|
|
197
|
+
? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
|
|
198
|
+
: getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
199
|
+
|
|
200
|
+
if (scanPhase === 'barcode') {
|
|
201
|
+
instructions = '';
|
|
202
|
+
}
|
|
203
|
+
|
|
105
204
|
const isLandscape = screenWidth > screenHeight;
|
|
106
205
|
let xMin, xMax, yMin, yMax;
|
|
107
206
|
|
|
@@ -109,50 +208,74 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
109
208
|
const boxWidthPercent = isLandscape ? 0.40 : 0.85;
|
|
110
209
|
xMin = isLandscape ? 30 : 7.5;
|
|
111
210
|
xMax = xMin + (boxWidthPercent * 100);
|
|
211
|
+
|
|
112
212
|
const containerWidth = Math.min(screenWidth * 0.95, 1000);
|
|
113
213
|
const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
|
|
214
|
+
|
|
114
215
|
const boxPixelWidth = containerWidth * boxWidthPercent;
|
|
115
216
|
const boxPixelHeight = boxPixelWidth / 1.59;
|
|
217
|
+
|
|
116
218
|
const heightPercent = (boxPixelHeight / containerHeight) * 100;
|
|
117
219
|
yMin = (100 - heightPercent) / 2.5;
|
|
118
220
|
yMax = yMin + heightPercent;
|
|
119
221
|
} else {
|
|
120
|
-
xMin = 5;
|
|
222
|
+
xMin = 5;
|
|
223
|
+
xMax = 95;
|
|
224
|
+
yMin = 34;
|
|
225
|
+
yMax = 66;
|
|
121
226
|
}
|
|
122
|
-
|
|
227
|
+
|
|
228
|
+
const platformBbox = {
|
|
229
|
+
xMin,
|
|
230
|
+
yMin,
|
|
231
|
+
xMax,
|
|
232
|
+
yMax,
|
|
233
|
+
borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60',
|
|
234
|
+
borderWidth: Platform.OS === 'web' ? 2 : 3,
|
|
235
|
+
cornerRadius: 8
|
|
236
|
+
};
|
|
237
|
+
|
|
123
238
|
return {
|
|
124
239
|
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
125
240
|
flashMode: 'auto' as const,
|
|
126
241
|
overlay: { guideText: instructions, bbox: platformBbox }
|
|
127
242
|
};
|
|
128
|
-
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
|
|
243
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight, scanPhase]);
|
|
129
244
|
|
|
130
245
|
const retakePicture = (sideToRetake: 'front' | 'back') => {
|
|
131
246
|
setIsProcessingCapture(false);
|
|
132
247
|
setProcessingImagePath(null);
|
|
248
|
+
scannedBarcodeRef.current = null;
|
|
249
|
+
setScanPhase('image');
|
|
250
|
+
setPendingCAImage(null);
|
|
133
251
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
134
252
|
setShowCamera(true);
|
|
135
253
|
refreshCamera();
|
|
136
254
|
actions.showCustomStepper(false);
|
|
137
|
-
setCapturedImages((prev) => {
|
|
138
|
-
|
|
255
|
+
setCapturedImages((prev) => {
|
|
256
|
+
const newState = { ...prev };
|
|
257
|
+
delete newState[sideToRetake];
|
|
258
|
+
return newState;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (value) {
|
|
262
|
+
const newValue = { ...value };
|
|
263
|
+
delete newValue[sideToRetake];
|
|
264
|
+
onValueChange(newValue);
|
|
265
|
+
}
|
|
139
266
|
};
|
|
140
267
|
|
|
141
268
|
const getCurrentSideVerification = (currentSide: string, countryKey: string) => {
|
|
142
269
|
const rawDocType = countrySelectionData?.documentType;
|
|
143
|
-
if (!rawDocType || !countryKey)
|
|
144
|
-
|
|
270
|
+
if (!rawDocType || !countryKey) return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
271
|
+
|
|
145
272
|
const jsonDocMap: Record<string, string> = {
|
|
146
|
-
'nationalId': 'identity_card',
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'driversLicense': 'drivers_licence',
|
|
150
|
-
'drivers_licence': 'drivers_licence',
|
|
151
|
-
'residencePermit': 'permanent_residence',
|
|
152
|
-
'healthInsuranceCard': 'health_insurance_card'
|
|
273
|
+
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
274
|
+
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
275
|
+
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
153
276
|
};
|
|
154
|
-
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
155
277
|
|
|
278
|
+
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
156
279
|
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
|
|
157
280
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
158
281
|
|
|
@@ -161,26 +284,24 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
161
284
|
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
162
285
|
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
163
286
|
}
|
|
164
|
-
if (!countryMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
165
|
-
|
|
166
|
-
// Use the translated name to look up the document in the JSON
|
|
167
|
-
const regionMapping = countryMapping[searchDocType];
|
|
168
|
-
if (!regionMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
169
287
|
|
|
288
|
+
if (!countryMapping || !countryMapping[searchDocType]) return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
289
|
+
|
|
170
290
|
const authMethod: string[] = [];
|
|
171
291
|
const mrzTypes: string[] = [];
|
|
172
|
-
const
|
|
173
|
-
|
|
292
|
+
const rawRegionKey = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
|
|
293
|
+
const key = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
294
|
+
const regionMapping = countryMapping[searchDocType];
|
|
295
|
+
|
|
174
296
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
175
297
|
regionMapping[key].forEach((item: any) => {
|
|
176
|
-
if (item[currentSide])
|
|
177
|
-
|
|
178
|
-
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
179
|
-
}
|
|
298
|
+
if (item[currentSide]) authMethod.push(item[currentSide]);
|
|
299
|
+
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
180
300
|
});
|
|
181
301
|
}
|
|
302
|
+
|
|
182
303
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
183
|
-
}
|
|
304
|
+
}
|
|
184
305
|
|
|
185
306
|
const getCorrespondingMrzType = (templatePath: string, mapping: any, regionKey: string = "root") => {
|
|
186
307
|
if (!mapping || !mapping[regionKey]) return null;
|
|
@@ -204,33 +325,38 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
204
325
|
if (isProcessingCapture) return;
|
|
205
326
|
setIsProcessingCapture(true);
|
|
206
327
|
setProcessingImagePath(capturePath);
|
|
328
|
+
|
|
207
329
|
try {
|
|
208
330
|
let imagePathForUpload = capturePath;
|
|
209
331
|
const overlayBbox = cameraConfig.overlay.bbox;
|
|
210
332
|
const uiCropBbox = {
|
|
211
|
-
minX: overlayBbox.xMin / 100,
|
|
212
|
-
minY: overlayBbox.yMin / 100,
|
|
333
|
+
minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
|
|
213
334
|
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
214
335
|
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
215
336
|
};
|
|
337
|
+
|
|
216
338
|
try {
|
|
217
339
|
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
|
|
218
340
|
} catch (e) {
|
|
219
341
|
console.warn("Crop failed, falling back to original image", e);
|
|
220
|
-
imagePathForUpload = capturePath;
|
|
342
|
+
imagePathForUpload = capturePath;
|
|
221
343
|
}
|
|
344
|
+
|
|
222
345
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
223
346
|
const newImages = {
|
|
224
347
|
...capturedImages,
|
|
225
348
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
226
349
|
};
|
|
350
|
+
|
|
227
351
|
setCapturedImages(newImages);
|
|
228
352
|
|
|
229
353
|
if (verified.country && verified.documentType) {
|
|
354
|
+
const docType = verified.documentType as string;
|
|
355
|
+
const resolvedDocumentType = GovernmentDocumentTypeBackend[docType] || (Object.values(GovernmentDocumentTypeBackend).includes(docType as GovernmentDocumentType) ? docType : null) || Object.entries(GovernmentDocumentTypeBackend).find(([, v]) => v === docType)?.[1] || docType;
|
|
230
356
|
onValueChange({
|
|
231
357
|
...newImages,
|
|
232
358
|
country: verified.country,
|
|
233
|
-
documentType:
|
|
359
|
+
documentType: resolvedDocumentType,
|
|
234
360
|
...(verified.region ? { region: verified.region } : {})
|
|
235
361
|
});
|
|
236
362
|
}
|
|
@@ -241,6 +367,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
241
367
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
242
368
|
setIsProcessingCapture(false);
|
|
243
369
|
setProcessingImagePath(null);
|
|
370
|
+
setScanPhase('image');
|
|
371
|
+
setPendingCAImage(null);
|
|
244
372
|
}, 600);
|
|
245
373
|
} catch (e: any) {
|
|
246
374
|
console.error("Backend Error:", e);
|
|
@@ -252,11 +380,100 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
252
380
|
}
|
|
253
381
|
};
|
|
254
382
|
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
383
|
+
const isBarcodeRequired = (): boolean => {
|
|
384
|
+
if (currentSide !== 'back') return false;
|
|
385
|
+
if (!countryData?.code || !selectedDocumentType?.type) return false;
|
|
386
|
+
|
|
387
|
+
const rawDocType = selectedDocumentType.type;
|
|
388
|
+
const jsonDocMap: Record<string, string> = {
|
|
389
|
+
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
390
|
+
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
391
|
+
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
395
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData.code] || countryData.code;
|
|
396
|
+
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
397
|
+
|
|
398
|
+
let countryMapping = baseMapping[rawCountryName];
|
|
399
|
+
if (!countryMapping) {
|
|
400
|
+
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryData.code.toLowerCase());
|
|
401
|
+
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!countryMapping || !countryMapping[searchDocType]) return false;
|
|
405
|
+
|
|
406
|
+
const rawRegionKey = countryData.region?.trim()?.length > 0 ? countryData.region.trim() : 'root';
|
|
407
|
+
const regionKey = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
408
|
+
const regionData = countryMapping[searchDocType][regionKey];
|
|
409
|
+
|
|
410
|
+
if (!regionData || !Array.isArray(regionData)) return false;
|
|
411
|
+
|
|
412
|
+
const requiresBarcode = regionData.some((item: any) => {
|
|
413
|
+
const method = String(item['back'] || '').toUpperCase();
|
|
414
|
+
return method.includes('BARCODE') || method.includes('2D');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
logger.log(`[Barcode Check] Dynamic check for ${countryData.code} - ${searchDocType} - ${regionKey}: requiresBarcode=${requiresBarcode}`);
|
|
418
|
+
return requiresBarcode;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const processCanadianBarcode = async (barcodeString: string) => {
|
|
422
|
+
if (isProcessingCapture || !pendingCAImage) return;
|
|
423
|
+
logger.log('[CA Barcode] Valid barcode decoded. Sending to parser...');
|
|
424
|
+
|
|
425
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
|
|
257
426
|
|
|
427
|
+
try {
|
|
428
|
+
const isHIC = selectedDocumentType!.type === 'health_insurance_card';
|
|
429
|
+
let parsedDictionary;
|
|
430
|
+
|
|
431
|
+
if (isHIC && barcodeString.length < 50) {
|
|
432
|
+
parsedDictionary = { data: { card_number: barcodeString, barcode_type: "1D" } };
|
|
433
|
+
} else {
|
|
434
|
+
const token = await authentification();
|
|
435
|
+
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeString, token }, env);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!parsedDictionary) throw new Error("Could not parse the barcode data.");
|
|
439
|
+
|
|
440
|
+
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
441
|
+
path: pendingCAImage.path,
|
|
442
|
+
templatePath: pendingCAImage.templatePath,
|
|
443
|
+
bbox: pendingCAImage.bbox,
|
|
444
|
+
success: true,
|
|
445
|
+
mrz: JSON.stringify(parsedDictionary),
|
|
446
|
+
barcode: barcodeString,
|
|
447
|
+
isAnalyzing: false,
|
|
448
|
+
country: countryData?.code,
|
|
449
|
+
documentType: selectedDocumentType!.type,
|
|
450
|
+
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
setSilentCaptureResult(verifiedResult);
|
|
454
|
+
if (pendingCAImage.bbox) {
|
|
455
|
+
const safeBbox = pendingCAImage.bbox as IBbox;
|
|
456
|
+
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await autoCapture(pendingCAImage.path, verifiedResult);
|
|
460
|
+
} catch (error: any) {
|
|
461
|
+
console.error("Barcode processing failed", error);
|
|
462
|
+
setScanPhase('image');
|
|
463
|
+
setPendingCAImage(null);
|
|
464
|
+
scannedBarcodeRef.current = null;
|
|
465
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
|
|
470
|
+
if (isProcessingCapture) return;
|
|
471
|
+
if (scanPhase === 'barcode') return;
|
|
472
|
+
if (silentCaptureResult.isAnalyzing) return;
|
|
473
|
+
|
|
258
474
|
if (result.success && result.path) {
|
|
259
475
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
476
|
+
|
|
260
477
|
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
261
478
|
let templateBbox: IBbox | undefined;
|
|
262
479
|
let templateResponse: any;
|
|
@@ -265,19 +482,20 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
265
482
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
266
483
|
return;
|
|
267
484
|
}
|
|
268
|
-
|
|
485
|
+
|
|
269
486
|
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
270
|
-
|
|
487
|
+
|
|
271
488
|
try {
|
|
489
|
+
// 1. Check & Crop the Template visually first
|
|
272
490
|
if (!templatePath) {
|
|
273
|
-
const templateType = await checkTemplateType({
|
|
274
|
-
path: result.path || '',
|
|
275
|
-
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
276
|
-
docRegion: countryData?.code || "",
|
|
491
|
+
const templateType = await checkTemplateType({
|
|
492
|
+
path: result.path || '',
|
|
493
|
+
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
494
|
+
docRegion: countryData?.code || "",
|
|
277
495
|
postfix: currentSide,
|
|
278
|
-
issuingSubdivision: issuingSubdivision
|
|
496
|
+
issuingSubdivision: issuingSubdivision
|
|
279
497
|
}, env);
|
|
280
|
-
|
|
498
|
+
|
|
281
499
|
templateResponse = templateType;
|
|
282
500
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
283
501
|
|
|
@@ -290,73 +508,95 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
290
508
|
try {
|
|
291
509
|
const crop = await cropByObb(result.path, (templateType as any).card_obb);
|
|
292
510
|
templateBbox = crop.bbox;
|
|
293
|
-
} catch {
|
|
511
|
+
} catch {}
|
|
294
512
|
}
|
|
295
513
|
}
|
|
296
|
-
|
|
514
|
+
|
|
297
515
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
298
516
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
299
517
|
let verificationRes: any;
|
|
300
|
-
|
|
518
|
+
|
|
301
519
|
if (currentSide === 'front') {
|
|
302
520
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
303
521
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
304
522
|
|
|
305
|
-
verificationRes = await frontVerification({
|
|
306
|
-
path: result.path,
|
|
307
|
-
regionMapping: {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
312
|
-
code: countryData?.code || '',
|
|
313
|
-
currentSide,
|
|
314
|
-
templatePath,
|
|
315
|
-
mrzType,
|
|
316
|
-
issuingSubdivision: issuingSubdivision
|
|
523
|
+
verificationRes = await frontVerification({
|
|
524
|
+
path: result.path,
|
|
525
|
+
regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
|
|
526
|
+
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
527
|
+
code: countryData?.code || '', currentSide, templatePath, mrzType,
|
|
528
|
+
issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode
|
|
317
529
|
}, env);
|
|
318
530
|
} else {
|
|
531
|
+
logger.log("Attempting backVerification with:", { path: result.path, templatePath, side: currentSide });
|
|
319
532
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
320
533
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
321
534
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
code: countryData?.code || '',
|
|
331
|
-
currentSide,
|
|
332
|
-
templatePath,
|
|
333
|
-
mrzType: backMrzType,
|
|
334
|
-
templateResponse,
|
|
335
|
-
issuingSubdivision: issuingSubdivision,
|
|
336
|
-
frontTemplatePath: frontTemplatePath
|
|
535
|
+
|
|
536
|
+
// This validates if the back matches the front template!
|
|
537
|
+
verificationRes = await backVerification({
|
|
538
|
+
path: result.path,
|
|
539
|
+
regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
|
|
540
|
+
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
541
|
+
code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse,
|
|
542
|
+
issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath
|
|
337
543
|
}, env);
|
|
338
544
|
}
|
|
339
|
-
|
|
545
|
+
|
|
340
546
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
547
|
+
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
548
|
+
|
|
549
|
+
if (isBarcodeRequired()) {
|
|
550
|
+
const barcodeData = scannedBarcodeRef.current;
|
|
551
|
+
const docTypeStr = selectedDocumentType!.type;
|
|
552
|
+
const isHIC = docTypeStr === 'health_insurance_card';
|
|
553
|
+
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
554
|
+
|
|
555
|
+
// If the barcode was caught extremely fast in the same frame
|
|
556
|
+
if (isValidScan) {
|
|
557
|
+
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
558
|
+
let parsedDictionary;
|
|
559
|
+
if (isHIC && barcodeData.length < 50) {
|
|
560
|
+
parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
|
|
561
|
+
} else {
|
|
562
|
+
const token = await authentification();
|
|
563
|
+
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
567
|
+
path: result.path, templatePath: templatePath, bbox: bbox, success: true,
|
|
568
|
+
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
569
|
+
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
570
|
+
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
setSilentCaptureResult(verifiedResult);
|
|
574
|
+
if (bbox) {
|
|
575
|
+
const safeBbox = bbox as IBbox;
|
|
576
|
+
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
577
|
+
}
|
|
578
|
+
await autoCapture(result.path!, verifiedResult);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// If validation passed but we still need the barcode, trigger the UI phase!
|
|
583
|
+
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
584
|
+
setPendingCAImage({ path: result.path!, templateResponse, templatePath, bbox });
|
|
585
|
+
setScanPhase('barcode');
|
|
586
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 4. NORMAL COMPLETION (For standard MRZ / Passport / Non-barcode documents)
|
|
591
|
+
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
592
|
+
path: result.path, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false,
|
|
593
|
+
country: countryData?.code, documentType: selectedDocumentType.type, region: issuingSubdivision
|
|
353
594
|
};
|
|
354
595
|
|
|
355
596
|
setSilentCaptureResult(verifiedResult);
|
|
356
597
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
357
|
-
|
|
358
598
|
await autoCapture(result.path, verifiedResult);
|
|
359
|
-
|
|
599
|
+
|
|
360
600
|
} catch (error: any) {
|
|
361
601
|
console.log("Backend Verification Error:", error);
|
|
362
602
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
@@ -368,15 +608,26 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
368
608
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
369
609
|
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
370
610
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
371
|
-
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
611
|
+
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
372
612
|
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
373
613
|
}
|
|
374
614
|
|
|
615
|
+
// The error is thrown here, completely blocking the switch to 'barcode' phase
|
|
375
616
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
376
617
|
}
|
|
377
618
|
}
|
|
378
619
|
};
|
|
379
620
|
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
|
|
623
|
+
const bData = silentCaptureResult.barcode;
|
|
624
|
+
if (typeof bData === 'string' && bData.length > 50) {
|
|
625
|
+
logger.log('[CA Barcode] Safety Net Triggered: Processing barcode.');
|
|
626
|
+
processCanadianBarcode(bData);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
|
|
630
|
+
|
|
380
631
|
const handleError = (event: { message: string }) => {
|
|
381
632
|
showAlert('Erreur', event.message);
|
|
382
633
|
setShowCamera(false);
|
|
@@ -387,6 +638,38 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
387
638
|
actions.showCustomStepper(!showCamera);
|
|
388
639
|
}, [showCamera]);
|
|
389
640
|
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
643
|
+
try {
|
|
644
|
+
const url = new URL(window.location.origin);
|
|
645
|
+
if (url.hostname === 'localhost') {
|
|
646
|
+
url.hostname = '192.168.0.33';
|
|
647
|
+
}
|
|
648
|
+
const params = new URLSearchParams(window.location.search);
|
|
649
|
+
params.set('session_id', state.session.session_id);
|
|
650
|
+
params.set('resume', 'true');
|
|
651
|
+
|
|
652
|
+
if (state.currentComponentIndex !== undefined) {
|
|
653
|
+
params.set('component_index', String(state.currentComponentIndex));
|
|
654
|
+
}
|
|
655
|
+
if (countryData?.code) {
|
|
656
|
+
params.set('country', countryData.code);
|
|
657
|
+
}
|
|
658
|
+
if (selectedDocumentType?.type) {
|
|
659
|
+
params.set('document_type', selectedDocumentType.type);
|
|
660
|
+
}
|
|
661
|
+
if (selectedDocumentType?.region && selectedDocumentType.region !== 'root') {
|
|
662
|
+
params.set('region', selectedDocumentType.region);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
url.hash = params.toString();
|
|
666
|
+
setCurrentUrl(url.toString());
|
|
667
|
+
} catch (e) {
|
|
668
|
+
console.error("Failed to construct QR handoff URL", e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}, [state.session.session_id, state.currentComponentIndex, countryData, selectedDocumentType]);
|
|
672
|
+
|
|
390
673
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
391
674
|
return (
|
|
392
675
|
<View style={styles.root}>
|
|
@@ -402,6 +685,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
402
685
|
|
|
403
686
|
if (showCamera) {
|
|
404
687
|
const isBusy = isProcessingCapture;
|
|
688
|
+
|
|
405
689
|
if (isRebootingCamera) {
|
|
406
690
|
return (
|
|
407
691
|
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
@@ -410,6 +694,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
410
694
|
</View>
|
|
411
695
|
);
|
|
412
696
|
}
|
|
697
|
+
|
|
413
698
|
return (
|
|
414
699
|
<View style={styles.root}>
|
|
415
700
|
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
@@ -423,7 +708,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
423
708
|
</Text>
|
|
424
709
|
</View>
|
|
425
710
|
</View>
|
|
426
|
-
|
|
711
|
+
|
|
427
712
|
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
428
713
|
{Platform.OS === 'web' && (
|
|
429
714
|
<View style={styles.webTopControls}>
|
|
@@ -432,7 +717,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
432
717
|
</TouchableOpacity>
|
|
433
718
|
</View>
|
|
434
719
|
)}
|
|
435
|
-
|
|
720
|
+
|
|
436
721
|
<EnhancedCameraView
|
|
437
722
|
key={`${currentSide}-${cameraKey}`}
|
|
438
723
|
showCamera={true}
|
|
@@ -442,32 +727,83 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
442
727
|
onError={handleError}
|
|
443
728
|
onSilentCapture={handleSilentCapture}
|
|
444
729
|
silentCaptureResult={silentCaptureResult}
|
|
730
|
+
isScanningBarcode={currentSide === 'back'}
|
|
731
|
+
barcodeTypes={targetBarcodeTypes}
|
|
732
|
+
onBarcodeScanned={(barcodeString) => {
|
|
733
|
+
const docTypeStr = selectedDocumentType?.type;
|
|
734
|
+
const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
|
|
735
|
+
if (isFullDataDoc && barcodeString.length < 50) return;
|
|
736
|
+
|
|
737
|
+
if (scannedBarcodeRef.current !== barcodeString) {
|
|
738
|
+
scannedBarcodeRef.current = barcodeString;
|
|
739
|
+
if (scanPhase === 'barcode' && pendingCAImage) {
|
|
740
|
+
processCanadianBarcode(barcodeString);
|
|
741
|
+
} else {
|
|
742
|
+
setSilentCaptureResult(prev => ({ ...prev, barcode: barcodeString }));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}}
|
|
445
746
|
overlayComponent={
|
|
446
747
|
<>
|
|
447
748
|
<IdCardOverlay
|
|
448
|
-
xMin={cameraConfig.overlay.bbox.xMin}
|
|
749
|
+
xMin={cameraConfig.overlay.bbox.xMin}
|
|
750
|
+
yMin={cameraConfig.overlay.bbox.yMin}
|
|
751
|
+
xMax={cameraConfig.overlay.bbox.xMax}
|
|
752
|
+
yMax={cameraConfig.overlay.bbox.yMax}
|
|
449
753
|
instructions={cameraConfig.overlay.guideText}
|
|
450
754
|
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
451
|
-
isSuccess={silentCaptureResult.success}
|
|
755
|
+
isSuccess={silentCaptureResult.success || scanPhase === 'barcode'}
|
|
452
756
|
language={state.currentLanguage}
|
|
453
757
|
stepperProps={{
|
|
454
758
|
back: () => {
|
|
455
759
|
if (currentSide === 'back') {
|
|
456
|
-
|
|
760
|
+
scannedBarcodeRef.current = null;
|
|
761
|
+
setScanPhase('image');
|
|
762
|
+
setPendingCAImage(null);
|
|
763
|
+
setCurrentSide('front');
|
|
764
|
+
setShowCamera(false);
|
|
765
|
+
setIsProcessingCapture(false);
|
|
766
|
+
setProcessingImagePath(null);
|
|
457
767
|
if (capturedImages['front']?.dir) {
|
|
458
768
|
const frontImage = capturedImages['front'];
|
|
459
|
-
setSilentCaptureResult((prev) => ({
|
|
460
|
-
|
|
461
|
-
|
|
769
|
+
setSilentCaptureResult((prev) => ({
|
|
770
|
+
...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front']
|
|
771
|
+
}));
|
|
772
|
+
} else {
|
|
773
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
actions.previousComponent();
|
|
777
|
+
}
|
|
462
778
|
},
|
|
463
779
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
464
|
-
step: state.currentComponentIndex + 1,
|
|
780
|
+
step: state.currentComponentIndex + 1,
|
|
781
|
+
totalSteps: state.template.components.length,
|
|
782
|
+
side: currentSide,
|
|
465
783
|
}}
|
|
466
784
|
/>
|
|
467
785
|
</>
|
|
468
786
|
}
|
|
469
787
|
/>
|
|
470
|
-
|
|
788
|
+
|
|
789
|
+
{scanPhase === 'barcode' && !isBusy && (
|
|
790
|
+
<View style={styles.barcodePromptContainer}>
|
|
791
|
+
<View style={styles.barcodePromptContent}>
|
|
792
|
+
<View style={styles.barcodeIconContainer}>
|
|
793
|
+
<Text style={styles.barcodeIcon}>✓</Text>
|
|
794
|
+
</View>
|
|
795
|
+
<View style={styles.barcodeTextContainer}>
|
|
796
|
+
<Text style={styles.barcodePromptTitle}>
|
|
797
|
+
{state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}
|
|
798
|
+
</Text>
|
|
799
|
+
<Text style={styles.barcodePromptDesc}>
|
|
800
|
+
{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}
|
|
801
|
+
</Text>
|
|
802
|
+
</View>
|
|
803
|
+
</View>
|
|
804
|
+
</View>
|
|
805
|
+
)}
|
|
806
|
+
|
|
471
807
|
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
472
808
|
<View style={styles.topAnalyzingPillContainer}>
|
|
473
809
|
<View style={styles.topAnalyzingPill}>
|
|
@@ -478,15 +814,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
478
814
|
</View>
|
|
479
815
|
</View>
|
|
480
816
|
)}
|
|
481
|
-
|
|
817
|
+
|
|
482
818
|
{isBusy && (
|
|
483
819
|
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
484
820
|
{processingImagePath && (
|
|
485
|
-
<Image
|
|
486
|
-
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
487
|
-
style={StyleSheet.absoluteFillObject}
|
|
488
|
-
resizeMode="cover"
|
|
489
|
-
/>
|
|
821
|
+
<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover" />
|
|
490
822
|
)}
|
|
491
823
|
<View style={styles.processingOverlay}>
|
|
492
824
|
<ActivityIndicator size="large" color="#2DBD60" />
|
|
@@ -496,7 +828,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
496
828
|
</View>
|
|
497
829
|
</View>
|
|
498
830
|
)}
|
|
499
|
-
|
|
831
|
+
|
|
500
832
|
{!isBusy && Platform.OS === 'web' ? (
|
|
501
833
|
<View style={styles.webBottomControlBar}>
|
|
502
834
|
{silentCaptureResult.error && (
|
|
@@ -518,15 +850,12 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
518
850
|
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
519
851
|
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
520
852
|
</TouchableOpacity>
|
|
521
|
-
<TouchableOpacity
|
|
522
|
-
style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]}
|
|
523
|
-
onPress={() => setShowCamera(false)}
|
|
524
|
-
>
|
|
853
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
525
854
|
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
526
855
|
</TouchableOpacity>
|
|
527
856
|
</View>
|
|
528
857
|
) : null}
|
|
529
|
-
|
|
858
|
+
|
|
530
859
|
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
|
|
531
860
|
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
532
861
|
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
@@ -549,6 +878,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
549
878
|
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
550
879
|
{getLocalizedText(component.instructions)}
|
|
551
880
|
</Text>
|
|
881
|
+
|
|
552
882
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
553
883
|
<View style={styles.imagePreviewWrapper}>
|
|
554
884
|
{capturedImages[currentSide]?.dir ? (
|
|
@@ -557,31 +887,80 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
557
887
|
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
558
888
|
) : null}
|
|
559
889
|
</View>
|
|
890
|
+
|
|
560
891
|
{!capturedImages[currentSide]?.dir && (
|
|
561
892
|
<View style={{ width: '100%', gap: 12 }}>
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
893
|
+
|
|
894
|
+
{isWebScanningRestricted ? (
|
|
895
|
+
<>
|
|
896
|
+
<View style={styles.restrictionWarningBanner}>
|
|
897
|
+
<Text style={styles.restrictionWarningTitle}>
|
|
898
|
+
{state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}
|
|
899
|
+
</Text>
|
|
900
|
+
<Text style={styles.restrictionWarningText}>
|
|
901
|
+
{state.currentLanguage === 'en'
|
|
902
|
+
? `This document can only be scanned using our mobile app.`
|
|
903
|
+
: `Ce document ne peut être scanné qu'avec notre application mobile.`}
|
|
904
|
+
</Text>
|
|
905
|
+
</View>
|
|
906
|
+
|
|
907
|
+
<Button
|
|
908
|
+
title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"}
|
|
909
|
+
onPress={() => setShowQRModal(true)}
|
|
910
|
+
variant="primary"
|
|
911
|
+
size="large"
|
|
912
|
+
fullWidth
|
|
913
|
+
/>
|
|
914
|
+
</>
|
|
915
|
+
) : (
|
|
916
|
+
/* 🟢 NORMAL FLOW: Show "Start Scanning" and optional web handoff */
|
|
917
|
+
<>
|
|
918
|
+
<Button
|
|
919
|
+
title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
|
|
920
|
+
onPress={() => {
|
|
921
|
+
setShowCamera(true);
|
|
922
|
+
refreshCamera();
|
|
923
|
+
actions.showCustomStepper(false);
|
|
924
|
+
}}
|
|
925
|
+
variant="primary"
|
|
926
|
+
size="large"
|
|
927
|
+
fullWidth
|
|
928
|
+
/>
|
|
929
|
+
|
|
930
|
+
{Platform.OS === 'web' && (
|
|
931
|
+
<Button
|
|
932
|
+
title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"}
|
|
933
|
+
onPress={() => setShowQRModal(true)}
|
|
934
|
+
variant="outline"
|
|
935
|
+
size="large"
|
|
936
|
+
fullWidth
|
|
937
|
+
/>
|
|
938
|
+
)}
|
|
939
|
+
</>
|
|
573
940
|
)}
|
|
941
|
+
|
|
574
942
|
</View>
|
|
575
943
|
)}
|
|
944
|
+
|
|
576
945
|
{capturedImages[currentSide]?.dir && (
|
|
577
946
|
<>
|
|
578
947
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth />
|
|
579
948
|
<Button title={t('common.next')} onPress={() => {
|
|
580
|
-
if (!selectedDocumentType) {
|
|
949
|
+
if (!selectedDocumentType) {
|
|
950
|
+
showAlert('Error', 'Document type not selected');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
581
953
|
if (currentSide === 'back' || selectedDocumentType.type === 'passport') {
|
|
582
954
|
actions.nextComponent();
|
|
583
955
|
} else {
|
|
584
|
-
setShowCamera(true);
|
|
956
|
+
setShowCamera(true);
|
|
957
|
+
refreshCamera();
|
|
958
|
+
setCurrentSide('back');
|
|
959
|
+
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
960
|
+
setIsProcessingCapture(false);
|
|
961
|
+
setProcessingImagePath(null);
|
|
962
|
+
setScanPhase('image');
|
|
963
|
+
setPendingCAImage(null);
|
|
585
964
|
}
|
|
586
965
|
}} variant="primary" size="large" fullWidth />
|
|
587
966
|
</>
|
|
@@ -590,22 +969,91 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
590
969
|
</View>
|
|
591
970
|
</ScrollView>
|
|
592
971
|
</View>
|
|
972
|
+
|
|
593
973
|
{showQRModal && Platform.OS === 'web' && (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
974
|
+
<View style={styles.qrModalOverlay}>
|
|
975
|
+
<View style={styles.qrModalContainer}>
|
|
976
|
+
<View style={styles.qrModalHeader}>
|
|
977
|
+
<Text style={styles.qrModalTitle}>
|
|
978
|
+
{state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
|
|
979
|
+
</Text>
|
|
980
|
+
<Text style={styles.qrModalText}>
|
|
981
|
+
{state.currentLanguage === 'en'
|
|
982
|
+
? "Scan the QR code to download our mobile app and complete the scanning process securely."
|
|
983
|
+
: "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
|
|
984
|
+
</Text>
|
|
985
|
+
</View>
|
|
986
|
+
|
|
987
|
+
<View style={styles.qrToggleContainer}>
|
|
988
|
+
<TouchableOpacity
|
|
989
|
+
style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]}
|
|
990
|
+
onPress={() => setQrPlatform('android')}
|
|
991
|
+
>
|
|
992
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
|
|
993
|
+
Android
|
|
994
|
+
</Text>
|
|
995
|
+
</TouchableOpacity>
|
|
996
|
+
<TouchableOpacity
|
|
997
|
+
style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]}
|
|
998
|
+
onPress={() => setQrPlatform('ios')}
|
|
999
|
+
>
|
|
1000
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
|
|
1001
|
+
iOS (Apple)
|
|
1002
|
+
</Text>
|
|
1003
|
+
</TouchableOpacity>
|
|
1004
|
+
</View>
|
|
1005
|
+
|
|
1006
|
+
<View style={styles.qrCodeWrapper}>
|
|
1007
|
+
<QRCode
|
|
1008
|
+
value={
|
|
1009
|
+
qrPlatform === 'android'
|
|
1010
|
+
? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
|
|
1011
|
+
: 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'
|
|
1012
|
+
}
|
|
1013
|
+
size={200} // Slightly reduced to accommodate the toggle switch
|
|
1014
|
+
backgroundColor="#FFFFFF"
|
|
1015
|
+
color="#0F172A"
|
|
1016
|
+
/>
|
|
1017
|
+
</View>
|
|
1018
|
+
|
|
1019
|
+
{isWebScanningRestricted && (
|
|
1020
|
+
<View style={styles.storeLinksContainer}>
|
|
1021
|
+
<Text style={styles.storeLinksTitle}>
|
|
1022
|
+
{state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
|
|
1023
|
+
</Text>
|
|
1024
|
+
<View style={styles.storeButtonsRow}>
|
|
1025
|
+
<TouchableOpacity
|
|
1026
|
+
style={styles.storeButton}
|
|
1027
|
+
onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')}
|
|
1028
|
+
>
|
|
1029
|
+
<Text style={styles.storeButtonText}>Google Play</Text>
|
|
1030
|
+
</TouchableOpacity>
|
|
1031
|
+
<TouchableOpacity
|
|
1032
|
+
style={styles.storeButton}
|
|
1033
|
+
onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')}
|
|
1034
|
+
>
|
|
1035
|
+
<Text style={styles.storeButtonText}>App Store</Text>
|
|
1036
|
+
</TouchableOpacity>
|
|
1037
|
+
</View>
|
|
1038
|
+
</View>
|
|
1039
|
+
)}
|
|
1040
|
+
|
|
1041
|
+
<View style={styles.listeningContainer}>
|
|
1042
|
+
<ActivityIndicator size="small" color="#2DBD60" />
|
|
1043
|
+
<Text style={styles.listeningText}>
|
|
1044
|
+
{state.currentLanguage === 'en' ? "Waiting for you to complete on mobile..." : "En attente de votre complétion sur mobile..."}
|
|
1045
|
+
</Text>
|
|
1046
|
+
</View>
|
|
1047
|
+
|
|
1048
|
+
<Button
|
|
1049
|
+
title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"}
|
|
1050
|
+
onPress={() => setShowQRModal(false)}
|
|
1051
|
+
variant="outline"
|
|
1052
|
+
fullWidth
|
|
1053
|
+
/>
|
|
604
1054
|
</View>
|
|
605
|
-
<Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth />
|
|
606
1055
|
</View>
|
|
607
|
-
|
|
608
|
-
)}
|
|
1056
|
+
)}
|
|
609
1057
|
</View>
|
|
610
1058
|
);
|
|
611
1059
|
};
|
|
@@ -646,9 +1094,82 @@ const styles = StyleSheet.create({
|
|
|
646
1094
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
647
1095
|
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 },
|
|
648
1096
|
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
649
|
-
qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(
|
|
650
|
-
qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth:
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1097
|
+
qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(15, 23, 42, 0.85)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(8px)' } as any }) },
|
|
1098
|
+
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 },
|
|
1099
|
+
qrModalHeader: { alignItems: 'center', marginBottom: 24 },
|
|
1100
|
+
qrModalTitle: { fontSize: 24, fontWeight: '800', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
|
|
1101
|
+
qrModalText: { fontSize: 15, color: '#475569', textAlign: 'center', lineHeight: 22 },
|
|
1102
|
+
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 },
|
|
1103
|
+
listeningContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#F0FDF4', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 12, marginBottom: 24, borderWidth: 1, borderColor: '#BBF7D0' },
|
|
1104
|
+
listeningText: { marginLeft: 12, color: '#166534', fontSize: 14, fontWeight: '600' },
|
|
1105
|
+
barcodePromptContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 150, paddingHorizontal: 24, },
|
|
1106
|
+
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)' } as any, ios: { backdropFilter: 'blur(8px)' } as any, }), },
|
|
1107
|
+
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)', },
|
|
1108
|
+
barcodeIcon: { fontSize: 16, color: '#2DBD60', fontWeight: 'bold', },
|
|
1109
|
+
barcodeTextContainer: { flex: 1, },
|
|
1110
|
+
barcodePromptTitle: { color: '#2DBD60', fontSize: 16, fontWeight: '700', marginBottom: 4, },
|
|
1111
|
+
barcodePromptDesc: { color: '#F8FAFC', fontSize: 14, fontWeight: '500', lineHeight: 20, },
|
|
1112
|
+
storeLinksContainer: { width: '100%', alignItems: 'center', marginBottom: 24, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E2E8F0' },
|
|
1113
|
+
storeLinksTitle: { fontSize: 14, color: '#64748B', fontWeight: '600', marginBottom: 12 },
|
|
1114
|
+
storeButtonsRow: { flexDirection: 'row', gap: 12, width: '100%', justifyContent: 'center' },
|
|
1115
|
+
storeButton: { flex: 1, backgroundColor: '#0F172A', paddingVertical: 12, borderRadius: 10, alignItems: 'center' },
|
|
1116
|
+
storeButtonText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 14 },
|
|
1117
|
+
restrictionWarningBanner: {
|
|
1118
|
+
backgroundColor: '#FFFBEB', // Light amber background
|
|
1119
|
+
borderWidth: 1,
|
|
1120
|
+
borderColor: '#F59E0B', // Amber border
|
|
1121
|
+
borderRadius: 12,
|
|
1122
|
+
padding: 16,
|
|
1123
|
+
marginBottom: 4,
|
|
1124
|
+
width: '100%',
|
|
1125
|
+
shadowColor: '#F59E0B',
|
|
1126
|
+
shadowOffset: { width: 0, height: 2 },
|
|
1127
|
+
shadowOpacity: 0.1,
|
|
1128
|
+
shadowRadius: 4,
|
|
1129
|
+
},
|
|
1130
|
+
restrictionWarningTitle: {
|
|
1131
|
+
color: '#B45309', // Dark amber text
|
|
1132
|
+
fontSize: 15,
|
|
1133
|
+
fontWeight: '800',
|
|
1134
|
+
marginBottom: 6,
|
|
1135
|
+
textAlign: 'center'
|
|
1136
|
+
},
|
|
1137
|
+
restrictionWarningText: {
|
|
1138
|
+
color: '#D97706',
|
|
1139
|
+
fontSize: 13,
|
|
1140
|
+
fontWeight: '500',
|
|
1141
|
+
textAlign: 'center',
|
|
1142
|
+
lineHeight: 18
|
|
1143
|
+
},
|
|
1144
|
+
qrToggleContainer: {
|
|
1145
|
+
flexDirection: 'row',
|
|
1146
|
+
backgroundColor: '#F1F5F9', // Light gray background
|
|
1147
|
+
borderRadius: 12,
|
|
1148
|
+
padding: 4,
|
|
1149
|
+
marginBottom: 20,
|
|
1150
|
+
width: '100%',
|
|
1151
|
+
},
|
|
1152
|
+
qrToggleButton: {
|
|
1153
|
+
flex: 1,
|
|
1154
|
+
paddingVertical: 10,
|
|
1155
|
+
borderRadius: 8,
|
|
1156
|
+
alignItems: 'center',
|
|
1157
|
+
justifyContent: 'center',
|
|
1158
|
+
},
|
|
1159
|
+
qrToggleButtonActive: {
|
|
1160
|
+
backgroundColor: '#FFFFFF',
|
|
1161
|
+
shadowColor: '#0F172A',
|
|
1162
|
+
shadowOffset: { width: 0, height: 2 },
|
|
1163
|
+
shadowOpacity: 0.08,
|
|
1164
|
+
shadowRadius: 4,
|
|
1165
|
+
elevation: 2,
|
|
1166
|
+
},
|
|
1167
|
+
qrToggleText: {
|
|
1168
|
+
fontSize: 14,
|
|
1169
|
+
fontWeight: '600',
|
|
1170
|
+
color: '#64748B', // Inactive text color
|
|
1171
|
+
},
|
|
1172
|
+
qrToggleTextActive: {
|
|
1173
|
+
color: '#0F172A', // Active text color
|
|
1174
|
+
},
|
|
654
1175
|
});
|