@sanctum-key/react-native-sdk 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +3 -1
- package/build/src/components/EnhancedCameraView.d.ts +7 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +88 -30
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/EnhancedCameraView.web.d.ts +5 -1
- package/build/src/components/EnhancedCameraView.web.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.web.js +80 -295
- package/build/src/components/EnhancedCameraView.web.js.map +1 -1
- package/build/src/components/KYCElements/CameraCapture.js +1 -1
- package/build/src/components/KYCElements/CameraCapture.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.js +38 -218
- package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +119 -26
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +512 -87
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +93 -51
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/components/OverLay/type.d.ts +1 -0
- package/build/src/components/OverLay/type.d.ts.map +1 -1
- package/build/src/components/OverLay/type.js.map +1 -1
- package/build/src/components/WebBarcodeScanner.d.ts +9 -0
- package/build/src/components/WebBarcodeScanner.d.ts.map +1 -0
- package/build/src/components/WebBarcodeScanner.js +152 -0
- package/build/src/components/WebBarcodeScanner.js.map +1 -0
- package/build/src/config/KYCConfigs.js +2 -2
- package/build/src/config/KYCConfigs.js.map +1 -1
- package/build/src/config/region_mapping.js +14 -14
- package/build/src/config/region_mapping.js.map +1 -1
- package/build/src/config/region_mapping.json +0 -14
- package/build/src/modules/api/CardAuthentification.d.ts +2 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +23 -40
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +7 -2
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +163 -57
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/types/KYC.types.d.ts +1 -0
- package/build/src/types/KYC.types.d.ts.map +1 -1
- package/build/src/types/KYC.types.js.map +1 -1
- package/build/src/utils/aamvaParser.d.ts +62 -0
- package/build/src/utils/aamvaParser.d.ts.map +1 -0
- package/build/src/utils/aamvaParser.js +91 -0
- package/build/src/utils/aamvaParser.js.map +1 -0
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +4 -3
- package/build/src/utils/cropByObb.js.map +1 -1
- package/build/src/web/WebKYCEntry.d.ts +1 -1
- package/build/src/web/WebKYCEntry.d.ts.map +1 -1
- package/build/src/web/WebKYCEntry.js +105 -239
- package/build/src/web/WebKYCEntry.js.map +1 -1
- package/package.json +3 -1
- package/src/components/EnhancedCameraView.tsx +116 -42
- package/src/components/EnhancedCameraView.web.tsx +98 -338
- package/src/components/KYCElements/CameraCapture.tsx +1 -1
- package/src/components/KYCElements/CountrySelection.tsx +60 -240
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +148 -29
- package/src/components/KYCElements/IDCardCapture.tsx +675 -142
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +112 -65
- package/src/components/OverLay/type.ts +1 -1
- package/src/components/WebBarcodeScanner.tsx +273 -0
- package/src/config/KYCConfigs.ts +2 -2
- package/src/config/region_mapping.json +0 -14
- package/src/config/region_mapping.ts +14 -14
- package/src/modules/api/CardAuthentification.ts +44 -58
- package/src/modules/api/KYCService.ts +236 -82
- package/src/types/KYC.types.ts +1 -0
- package/src/utils/aamvaParser.ts +102 -0
- package/src/utils/cropByObb.ts +5 -3
- package/src/web/WebKYCEntry.tsx +125 -301
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
@@ -11,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,39 +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
|
+
|
|
272
|
+
const jsonDocMap: Record<string, string> = {
|
|
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'
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
145
279
|
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
|
|
146
280
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
147
281
|
|
|
@@ -150,25 +284,24 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
150
284
|
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
151
285
|
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
152
286
|
}
|
|
153
|
-
if (!countryMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
154
|
-
|
|
155
|
-
const regionMapping = countryMapping[rawDocType];
|
|
156
|
-
if (!regionMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
157
287
|
|
|
288
|
+
if (!countryMapping || !countryMapping[searchDocType]) return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
289
|
+
|
|
158
290
|
const authMethod: string[] = [];
|
|
159
291
|
const mrzTypes: string[] = [];
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
+
|
|
162
296
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
163
297
|
regionMapping[key].forEach((item: any) => {
|
|
164
|
-
if (item[currentSide])
|
|
165
|
-
|
|
166
|
-
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
167
|
-
}
|
|
298
|
+
if (item[currentSide]) authMethod.push(item[currentSide]);
|
|
299
|
+
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
168
300
|
});
|
|
169
301
|
}
|
|
302
|
+
|
|
170
303
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
171
|
-
}
|
|
304
|
+
}
|
|
172
305
|
|
|
173
306
|
const getCorrespondingMrzType = (templatePath: string, mapping: any, regionKey: string = "root") => {
|
|
174
307
|
if (!mapping || !mapping[regionKey]) return null;
|
|
@@ -192,33 +325,38 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
192
325
|
if (isProcessingCapture) return;
|
|
193
326
|
setIsProcessingCapture(true);
|
|
194
327
|
setProcessingImagePath(capturePath);
|
|
328
|
+
|
|
195
329
|
try {
|
|
196
330
|
let imagePathForUpload = capturePath;
|
|
197
331
|
const overlayBbox = cameraConfig.overlay.bbox;
|
|
198
332
|
const uiCropBbox = {
|
|
199
|
-
minX: overlayBbox.xMin / 100,
|
|
200
|
-
minY: overlayBbox.yMin / 100,
|
|
333
|
+
minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100,
|
|
201
334
|
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
202
335
|
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
203
336
|
};
|
|
337
|
+
|
|
204
338
|
try {
|
|
205
339
|
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
|
|
206
340
|
} catch (e) {
|
|
207
341
|
console.warn("Crop failed, falling back to original image", e);
|
|
208
|
-
imagePathForUpload = capturePath;
|
|
342
|
+
imagePathForUpload = capturePath;
|
|
209
343
|
}
|
|
344
|
+
|
|
210
345
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
211
346
|
const newImages = {
|
|
212
347
|
...capturedImages,
|
|
213
348
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
214
349
|
};
|
|
350
|
+
|
|
215
351
|
setCapturedImages(newImages);
|
|
216
352
|
|
|
217
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;
|
|
218
356
|
onValueChange({
|
|
219
357
|
...newImages,
|
|
220
358
|
country: verified.country,
|
|
221
|
-
documentType:
|
|
359
|
+
documentType: resolvedDocumentType,
|
|
222
360
|
...(verified.region ? { region: verified.region } : {})
|
|
223
361
|
});
|
|
224
362
|
}
|
|
@@ -229,6 +367,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
229
367
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
230
368
|
setIsProcessingCapture(false);
|
|
231
369
|
setProcessingImagePath(null);
|
|
370
|
+
setScanPhase('image');
|
|
371
|
+
setPendingCAImage(null);
|
|
232
372
|
}, 600);
|
|
233
373
|
} catch (e: any) {
|
|
234
374
|
console.error("Backend Error:", e);
|
|
@@ -240,11 +380,100 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
240
380
|
}
|
|
241
381
|
};
|
|
242
382
|
|
|
243
|
-
const
|
|
244
|
-
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
|
+
};
|
|
245
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: '' }));
|
|
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
|
+
|
|
246
474
|
if (result.success && result.path) {
|
|
247
475
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
476
|
+
|
|
248
477
|
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
249
478
|
let templateBbox: IBbox | undefined;
|
|
250
479
|
let templateResponse: any;
|
|
@@ -253,19 +482,20 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
253
482
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
254
483
|
return;
|
|
255
484
|
}
|
|
256
|
-
|
|
485
|
+
|
|
257
486
|
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
258
|
-
|
|
487
|
+
|
|
259
488
|
try {
|
|
489
|
+
// 1. Check & Crop the Template visually first
|
|
260
490
|
if (!templatePath) {
|
|
261
|
-
const templateType = await checkTemplateType({
|
|
262
|
-
path: result.path || '',
|
|
263
|
-
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
264
|
-
docRegion: countryData?.code || "",
|
|
491
|
+
const templateType = await checkTemplateType({
|
|
492
|
+
path: result.path || '',
|
|
493
|
+
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
494
|
+
docRegion: countryData?.code || "",
|
|
265
495
|
postfix: currentSide,
|
|
266
|
-
issuingSubdivision: issuingSubdivision
|
|
496
|
+
issuingSubdivision: issuingSubdivision
|
|
267
497
|
}, env);
|
|
268
|
-
|
|
498
|
+
|
|
269
499
|
templateResponse = templateType;
|
|
270
500
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
271
501
|
|
|
@@ -278,73 +508,95 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
278
508
|
try {
|
|
279
509
|
const crop = await cropByObb(result.path, (templateType as any).card_obb);
|
|
280
510
|
templateBbox = crop.bbox;
|
|
281
|
-
} catch {
|
|
511
|
+
} catch {}
|
|
282
512
|
}
|
|
283
513
|
}
|
|
284
|
-
|
|
514
|
+
|
|
285
515
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
286
516
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
287
517
|
let verificationRes: any;
|
|
288
|
-
|
|
518
|
+
|
|
289
519
|
if (currentSide === 'front') {
|
|
290
520
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
291
521
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
292
522
|
|
|
293
|
-
verificationRes = await frontVerification({
|
|
294
|
-
path: result.path,
|
|
295
|
-
regionMapping: {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
300
|
-
code: countryData?.code || '',
|
|
301
|
-
currentSide,
|
|
302
|
-
templatePath,
|
|
303
|
-
mrzType,
|
|
304
|
-
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
|
|
305
529
|
}, env);
|
|
306
530
|
} else {
|
|
531
|
+
logger.log("Attempting backVerification with:", { path: result.path, templatePath, side: currentSide });
|
|
307
532
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
308
533
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
309
534
|
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
code: countryData?.code || '',
|
|
319
|
-
currentSide,
|
|
320
|
-
templatePath,
|
|
321
|
-
mrzType: backMrzType,
|
|
322
|
-
templateResponse,
|
|
323
|
-
issuingSubdivision: issuingSubdivision,
|
|
324
|
-
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
|
|
325
543
|
}, env);
|
|
326
544
|
}
|
|
327
|
-
|
|
545
|
+
|
|
328
546
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
341
594
|
};
|
|
342
595
|
|
|
343
596
|
setSilentCaptureResult(verifiedResult);
|
|
344
597
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
345
|
-
|
|
346
598
|
await autoCapture(result.path, verifiedResult);
|
|
347
|
-
|
|
599
|
+
|
|
348
600
|
} catch (error: any) {
|
|
349
601
|
console.log("Backend Verification Error:", error);
|
|
350
602
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
@@ -356,15 +608,26 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
356
608
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
357
609
|
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
358
610
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
359
|
-
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
611
|
+
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
360
612
|
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
361
613
|
}
|
|
362
614
|
|
|
615
|
+
// The error is thrown here, completely blocking the switch to 'barcode' phase
|
|
363
616
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
364
617
|
}
|
|
365
618
|
}
|
|
366
619
|
};
|
|
367
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
|
+
|
|
368
631
|
const handleError = (event: { message: string }) => {
|
|
369
632
|
showAlert('Erreur', event.message);
|
|
370
633
|
setShowCamera(false);
|
|
@@ -375,6 +638,38 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
375
638
|
actions.showCustomStepper(!showCamera);
|
|
376
639
|
}, [showCamera]);
|
|
377
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
|
+
|
|
378
673
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
379
674
|
return (
|
|
380
675
|
<View style={styles.root}>
|
|
@@ -390,6 +685,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
390
685
|
|
|
391
686
|
if (showCamera) {
|
|
392
687
|
const isBusy = isProcessingCapture;
|
|
688
|
+
|
|
393
689
|
if (isRebootingCamera) {
|
|
394
690
|
return (
|
|
395
691
|
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
@@ -398,6 +694,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
398
694
|
</View>
|
|
399
695
|
);
|
|
400
696
|
}
|
|
697
|
+
|
|
401
698
|
return (
|
|
402
699
|
<View style={styles.root}>
|
|
403
700
|
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
@@ -411,7 +708,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
411
708
|
</Text>
|
|
412
709
|
</View>
|
|
413
710
|
</View>
|
|
414
|
-
|
|
711
|
+
|
|
415
712
|
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
416
713
|
{Platform.OS === 'web' && (
|
|
417
714
|
<View style={styles.webTopControls}>
|
|
@@ -420,7 +717,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
420
717
|
</TouchableOpacity>
|
|
421
718
|
</View>
|
|
422
719
|
)}
|
|
423
|
-
|
|
720
|
+
|
|
424
721
|
<EnhancedCameraView
|
|
425
722
|
key={`${currentSide}-${cameraKey}`}
|
|
426
723
|
showCamera={true}
|
|
@@ -430,32 +727,83 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
430
727
|
onError={handleError}
|
|
431
728
|
onSilentCapture={handleSilentCapture}
|
|
432
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
|
+
}}
|
|
433
746
|
overlayComponent={
|
|
434
747
|
<>
|
|
435
748
|
<IdCardOverlay
|
|
436
|
-
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}
|
|
437
753
|
instructions={cameraConfig.overlay.guideText}
|
|
438
754
|
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
439
|
-
isSuccess={silentCaptureResult.success}
|
|
755
|
+
isSuccess={silentCaptureResult.success || scanPhase === 'barcode'}
|
|
440
756
|
language={state.currentLanguage}
|
|
441
757
|
stepperProps={{
|
|
442
758
|
back: () => {
|
|
443
759
|
if (currentSide === 'back') {
|
|
444
|
-
|
|
760
|
+
scannedBarcodeRef.current = null;
|
|
761
|
+
setScanPhase('image');
|
|
762
|
+
setPendingCAImage(null);
|
|
763
|
+
setCurrentSide('front');
|
|
764
|
+
setShowCamera(false);
|
|
765
|
+
setIsProcessingCapture(false);
|
|
766
|
+
setProcessingImagePath(null);
|
|
445
767
|
if (capturedImages['front']?.dir) {
|
|
446
768
|
const frontImage = capturedImages['front'];
|
|
447
|
-
setSilentCaptureResult((prev) => ({
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
778
|
},
|
|
451
779
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
452
|
-
step: state.currentComponentIndex + 1,
|
|
780
|
+
step: state.currentComponentIndex + 1,
|
|
781
|
+
totalSteps: state.template.components.length,
|
|
782
|
+
side: currentSide,
|
|
453
783
|
}}
|
|
454
784
|
/>
|
|
455
785
|
</>
|
|
456
786
|
}
|
|
457
787
|
/>
|
|
458
|
-
|
|
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
|
+
|
|
459
807
|
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
460
808
|
<View style={styles.topAnalyzingPillContainer}>
|
|
461
809
|
<View style={styles.topAnalyzingPill}>
|
|
@@ -466,15 +814,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
466
814
|
</View>
|
|
467
815
|
</View>
|
|
468
816
|
)}
|
|
469
|
-
|
|
817
|
+
|
|
470
818
|
{isBusy && (
|
|
471
819
|
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
472
820
|
{processingImagePath && (
|
|
473
|
-
<Image
|
|
474
|
-
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
475
|
-
style={StyleSheet.absoluteFillObject}
|
|
476
|
-
resizeMode="cover"
|
|
477
|
-
/>
|
|
821
|
+
<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover" />
|
|
478
822
|
)}
|
|
479
823
|
<View style={styles.processingOverlay}>
|
|
480
824
|
<ActivityIndicator size="large" color="#2DBD60" />
|
|
@@ -484,7 +828,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
484
828
|
</View>
|
|
485
829
|
</View>
|
|
486
830
|
)}
|
|
487
|
-
|
|
831
|
+
|
|
488
832
|
{!isBusy && Platform.OS === 'web' ? (
|
|
489
833
|
<View style={styles.webBottomControlBar}>
|
|
490
834
|
{silentCaptureResult.error && (
|
|
@@ -506,15 +850,12 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
506
850
|
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
507
851
|
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
508
852
|
</TouchableOpacity>
|
|
509
|
-
<TouchableOpacity
|
|
510
|
-
style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]}
|
|
511
|
-
onPress={() => setShowCamera(false)}
|
|
512
|
-
>
|
|
853
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
513
854
|
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
514
855
|
</TouchableOpacity>
|
|
515
856
|
</View>
|
|
516
857
|
) : null}
|
|
517
|
-
|
|
858
|
+
|
|
518
859
|
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
|
|
519
860
|
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
520
861
|
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
@@ -537,6 +878,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
537
878
|
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
538
879
|
{getLocalizedText(component.instructions)}
|
|
539
880
|
</Text>
|
|
881
|
+
|
|
540
882
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
541
883
|
<View style={styles.imagePreviewWrapper}>
|
|
542
884
|
{capturedImages[currentSide]?.dir ? (
|
|
@@ -545,31 +887,80 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
545
887
|
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
546
888
|
) : null}
|
|
547
889
|
</View>
|
|
890
|
+
|
|
548
891
|
{!capturedImages[currentSide]?.dir && (
|
|
549
892
|
<View style={{ width: '100%', gap: 12 }}>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
</>
|
|
561
940
|
)}
|
|
941
|
+
|
|
562
942
|
</View>
|
|
563
943
|
)}
|
|
944
|
+
|
|
564
945
|
{capturedImages[currentSide]?.dir && (
|
|
565
946
|
<>
|
|
566
947
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth />
|
|
567
948
|
<Button title={t('common.next')} onPress={() => {
|
|
568
|
-
if (!selectedDocumentType) {
|
|
949
|
+
if (!selectedDocumentType) {
|
|
950
|
+
showAlert('Error', 'Document type not selected');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
569
953
|
if (currentSide === 'back' || selectedDocumentType.type === 'passport') {
|
|
570
954
|
actions.nextComponent();
|
|
571
955
|
} else {
|
|
572
|
-
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);
|
|
573
964
|
}
|
|
574
965
|
}} variant="primary" size="large" fullWidth />
|
|
575
966
|
</>
|
|
@@ -578,22 +969,91 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
578
969
|
</View>
|
|
579
970
|
</ScrollView>
|
|
580
971
|
</View>
|
|
972
|
+
|
|
581
973
|
{showQRModal && Platform.OS === 'web' && (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
/>
|
|
592
1054
|
</View>
|
|
593
|
-
<Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth />
|
|
594
1055
|
</View>
|
|
595
|
-
|
|
596
|
-
)}
|
|
1056
|
+
)}
|
|
597
1057
|
</View>
|
|
598
1058
|
);
|
|
599
1059
|
};
|
|
@@ -634,9 +1094,82 @@ const styles = StyleSheet.create({
|
|
|
634
1094
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
635
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 },
|
|
636
1096
|
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
637
|
-
qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(
|
|
638
|
-
qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
},
|
|
642
1175
|
});
|