@sanctum-key/react-native-sdk 1.0.37 → 1.0.38
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 +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +191 -413
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +308 -689
|
@@ -14,13 +14,12 @@ import pathToBase64 from '../../utils/pathToBase64';
|
|
|
14
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 {
|
|
18
|
-
import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
17
|
+
import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
|
|
19
18
|
import kycService, { authentification } from '../../modules/api/KYCService';
|
|
20
19
|
|
|
21
20
|
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',
|
|
21
|
+
'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
|
|
22
|
+
'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
|
|
24
23
|
'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
|
|
25
24
|
};
|
|
26
25
|
|
|
@@ -41,17 +40,14 @@ interface IDCardCaptureProps {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
const SHORT_TO_LONG_REGION: Record<string, string> = {
|
|
44
|
-
"AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
|
|
43
|
+
"AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba", "NB": "New Brunswick",
|
|
44
|
+
"NL": "Newfoundland and Labrador", "NT": "Northwest Territories", "NS": "Nova Scotia",
|
|
45
|
+
"NU": "Nunavut", "ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
|
|
48
46
|
"SK": "Saskatchewan", "YT": "Yukon"
|
|
49
47
|
};
|
|
50
48
|
|
|
51
|
-
// 🚨 Isolates the 1-second re-renders away from the Camera!
|
|
52
49
|
const ExpiringCodeTimer = ({ initialCode, language }: { initialCode: string, language: string }) => {
|
|
53
50
|
const [timeLeft, setTimeLeft] = useState(300);
|
|
54
|
-
|
|
55
51
|
useEffect(() => {
|
|
56
52
|
if (timeLeft <= 0) return;
|
|
57
53
|
const timer = setInterval(() => setTimeLeft((prev) => prev - 1), 1000);
|
|
@@ -65,7 +61,6 @@ const ExpiringCodeTimer = ({ initialCode, language }: { initialCode: string, lan
|
|
|
65
61
|
};
|
|
66
62
|
|
|
67
63
|
if (timeLeft <= 0) return null;
|
|
68
|
-
|
|
69
64
|
return (
|
|
70
65
|
<View style={styles.activeCodeContainer}>
|
|
71
66
|
<Text style={styles.activeCodeText}>{initialCode}</Text>
|
|
@@ -83,6 +78,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
83
78
|
const [currentSide, setCurrentSide] = useState<'front' | 'back'>('front');
|
|
84
79
|
const [bboxBySide, setBboxBySide] = useState<Record<string, IBbox>>({});
|
|
85
80
|
const [silentCaptureResult, setSilentCaptureResult] = useState<ISilentCaptureResult>({ success: false, isAnalyzing: false });
|
|
81
|
+
|
|
86
82
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
87
83
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
88
84
|
const [cameraKey, setCameraKey] = useState(0);
|
|
@@ -91,45 +87,71 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
91
87
|
const [showQRModal, setShowQRModal] = useState(false);
|
|
92
88
|
const [currentUrl, setCurrentUrl] = useState('');
|
|
93
89
|
const [qrPlatform, setQrPlatform] = useState<'android' | 'ios'>('android');
|
|
94
|
-
|
|
95
90
|
const [scanPhase, setScanPhase] = useState<'image' | 'barcode'>('image');
|
|
96
91
|
const [pendingCAImage, setPendingCAImage] = useState<{ path: string, templateResponse: any, templatePath: string, bbox?: IBbox } | null>(null);
|
|
97
|
-
|
|
98
92
|
const scannedBarcodeRef = useRef<string | null>(null);
|
|
99
|
-
|
|
93
|
+
const [generatedCode, setGeneratedCode] = useState<string | null>(null);
|
|
94
|
+
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
|
95
|
+
|
|
96
|
+
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
97
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
98
|
+
|
|
99
|
+
console.log(currentUrl)
|
|
100
|
+
|
|
101
|
+
const countrySelectionData = useMemo(() => {
|
|
102
|
+
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
103
|
+
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
104
|
+
}, [state.template.components, state.componentData]);
|
|
105
|
+
|
|
106
|
+
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
107
|
+
'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
|
|
108
|
+
'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
109
|
+
'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
|
|
110
|
+
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const selectedDocumentType = useMemo<{ type: GovernmentDocumentType; region: string } | null>(() => {
|
|
114
|
+
if (!countrySelectionData?.documentType) return null;
|
|
115
|
+
const backendDocType = countrySelectionData.documentType;
|
|
116
|
+
const mappedType = documentTypeMapping[backendDocType] || backendDocType as GovernmentDocumentType;
|
|
117
|
+
return { type: mappedType, region: countrySelectionData.region || 'root' };
|
|
118
|
+
}, [countrySelectionData, documentTypeMapping]);
|
|
119
|
+
|
|
120
|
+
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
121
|
+
|
|
122
|
+
// ==========================================
|
|
123
|
+
// 🚨 CRITICAL FIX: REF MIRRORS FOR IOS BRIDGE
|
|
124
|
+
// Bypass stale native closures by mirroring all state into refs
|
|
125
|
+
// ==========================================
|
|
100
126
|
const isAnalyzingRef = useRef<boolean>(false);
|
|
101
127
|
const isProcessingCaptureRef = useRef<boolean>(false);
|
|
102
128
|
const scanPhaseRef = useRef<'image' | 'barcode'>('image');
|
|
129
|
+
const currentSideRef = useRef(currentSide);
|
|
130
|
+
const capturedImagesRef = useRef(capturedImages);
|
|
131
|
+
const selectedDocTypeRef = useRef(selectedDocumentType);
|
|
132
|
+
const countryDataRef = useRef(countryData);
|
|
103
133
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
useEffect(() => { isAnalyzingRef.current = silentCaptureResult.isAnalyzing ?? false; }, [silentCaptureResult.isAnalyzing]);
|
|
135
|
+
useEffect(() => { isProcessingCaptureRef.current = isProcessingCapture; }, [isProcessingCapture]);
|
|
136
|
+
useEffect(() => { scanPhaseRef.current = scanPhase; }, [scanPhase]);
|
|
137
|
+
useEffect(() => { currentSideRef.current = currentSide; }, [currentSide]);
|
|
138
|
+
useEffect(() => { capturedImagesRef.current = capturedImages; }, [capturedImages]);
|
|
139
|
+
useEffect(() => { selectedDocTypeRef.current = selectedDocumentType; }, [selectedDocumentType]);
|
|
140
|
+
useEffect(() => { countryDataRef.current = countryData; }, [countryData]);
|
|
141
|
+
// ==========================================
|
|
107
142
|
|
|
108
143
|
const handleGenerateCode = async () => {
|
|
109
144
|
if (isGeneratingCode) return;
|
|
110
145
|
setIsGeneratingCode(true);
|
|
111
|
-
|
|
112
146
|
try {
|
|
113
147
|
if (!apiKey) {
|
|
114
148
|
showAlert('Error', 'API Key is missing.');
|
|
115
|
-
setIsGeneratingCode(false);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Pass the dynamic apiKey to your service
|
|
120
|
-
const res = await kycService.generateReferralCode(
|
|
121
|
-
{ session_id: state.session.session_id },
|
|
122
|
-
apiKey
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// Extract the code from the response
|
|
126
|
-
const newCode = res.code || res.referral_code || res?.data?.code || res?.data?.referral_code;
|
|
127
|
-
|
|
128
|
-
if (newCode) {
|
|
129
|
-
setGeneratedCode(newCode);
|
|
130
|
-
} else {
|
|
131
|
-
showAlert('Error', 'Failed to parse code from server response.');
|
|
149
|
+
setIsGeneratingCode(false); return;
|
|
132
150
|
}
|
|
151
|
+
const res = await kycService.generateReferralCode({ session_id: state.session.session_id }, apiKey);
|
|
152
|
+
const newCode = res.code || res.referral_code || res?.data?.code || res?.data?.referral_code;
|
|
153
|
+
if (newCode) setGeneratedCode(newCode);
|
|
154
|
+
else showAlert('Error', 'Failed to parse code from server response.');
|
|
133
155
|
} catch (error: any) {
|
|
134
156
|
showAlert('Error', error.message || 'Failed to generate code');
|
|
135
157
|
} finally {
|
|
@@ -139,56 +161,18 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
139
161
|
|
|
140
162
|
useEffect(() => {
|
|
141
163
|
if (typeof window !== 'undefined' && window.location) setCurrentUrl(window.location.href);
|
|
142
|
-
console.log(currentUrl)
|
|
143
164
|
}, []);
|
|
144
165
|
|
|
145
|
-
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
146
|
-
'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
|
|
147
|
-
'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
148
|
-
'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
|
|
149
|
-
'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
|
|
153
|
-
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
154
|
-
|
|
155
166
|
const getLocalizedText = (text: LocalizedText | Record<string, LocalizedText>): string => {
|
|
156
|
-
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
|
|
157
|
-
return text[currentSide][locale] || '';
|
|
167
|
+
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale]) return text[currentSide][locale] || '';
|
|
158
168
|
return "";
|
|
159
169
|
};
|
|
160
170
|
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
163
|
-
const baseUrl = window.location.origin + window.location.pathname;
|
|
164
|
-
const handoffUrl = `${baseUrl}?session_id=${state.session.session_id}&resume=true`;
|
|
165
|
-
setCurrentUrl(handoffUrl);
|
|
166
|
-
}
|
|
167
|
-
}, [state.session.session_id]);
|
|
168
|
-
|
|
169
|
-
const countrySelectionData = useMemo(() => {
|
|
170
|
-
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
171
|
-
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
172
|
-
}, [state.template.components, state.componentData]);
|
|
173
|
-
|
|
174
|
-
const selectedDocumentType = useMemo<{ type: GovernmentDocumentType; region: string } | null>(() => {
|
|
175
|
-
if (!countrySelectionData?.documentType) return null;
|
|
176
|
-
const backendDocType = countrySelectionData.documentType;
|
|
177
|
-
const mappedType = documentTypeMapping[backendDocType] || backendDocType as GovernmentDocumentType;
|
|
178
|
-
return { type: mappedType, region: countrySelectionData.region || 'root' };
|
|
179
|
-
}, [countrySelectionData, documentTypeMapping]);
|
|
180
|
-
|
|
181
|
-
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
182
|
-
|
|
183
171
|
const isWebScanningRestricted = useMemo(() => {
|
|
184
172
|
if (Platform.OS !== 'web') return false;
|
|
185
173
|
if (countryData?.code !== 'CA') return false;
|
|
186
|
-
|
|
187
174
|
const docType = selectedDocumentType?.type;
|
|
188
|
-
return docType === 'drivers_licence' ||
|
|
189
|
-
docType === 'health_insurance_card' ||
|
|
190
|
-
docType === 'national_id' ||
|
|
191
|
-
docType === 'identity_card';
|
|
175
|
+
return docType === 'drivers_licence' || docType === 'health_insurance_card' || docType === 'national_id' || docType === 'identity_card';
|
|
192
176
|
}, [countryData?.code, selectedDocumentType?.type]);
|
|
193
177
|
|
|
194
178
|
const targetBarcodeTypes = useMemo<any[]>(() => {
|
|
@@ -199,35 +183,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
199
183
|
|
|
200
184
|
const refreshCamera = () => {
|
|
201
185
|
setIsRebootingCamera(true);
|
|
202
|
-
|
|
203
186
|
if (scanPhase === 'barcode') {
|
|
204
187
|
setScanPhase('image');
|
|
205
188
|
setPendingCAImage(null);
|
|
206
189
|
scannedBarcodeRef.current = null;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
delete newState[currentSide];
|
|
211
|
-
return newState;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (value) {
|
|
215
|
-
const newValue = { ...value };
|
|
216
|
-
delete newValue[currentSide];
|
|
217
|
-
onValueChange(newValue);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
setSilentCaptureResult({
|
|
221
|
-
success: false,
|
|
222
|
-
isAnalyzing: false,
|
|
223
|
-
path: '',
|
|
224
|
-
error: '',
|
|
225
|
-
templatePath: '',
|
|
226
|
-
mrz: '',
|
|
227
|
-
bbox: undefined
|
|
228
|
-
});
|
|
190
|
+
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[currentSide]; return newState; });
|
|
191
|
+
if (value) { const newValue = { ...value }; delete newValue[currentSide]; onValueChange(newValue); }
|
|
192
|
+
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
229
193
|
}
|
|
230
|
-
|
|
231
194
|
setTimeout(() => {
|
|
232
195
|
setCameraKey(prev => prev + 1);
|
|
233
196
|
setIsRebootingCamera(false);
|
|
@@ -240,116 +203,57 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
240
203
|
};
|
|
241
204
|
|
|
242
205
|
useEffect(() => {
|
|
243
|
-
if (value && Object.keys(value).length > 0) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (currentImageData?.dir) {
|
|
250
|
-
setSilentCaptureResult(prev => ({
|
|
251
|
-
...prev,
|
|
252
|
-
path: currentImageData.dir,
|
|
253
|
-
success: true,
|
|
254
|
-
isAnalyzing: false,
|
|
255
|
-
mrz: currentImageData.mrz || '',
|
|
256
|
-
templatePath: currentImageData.templatePath || '',
|
|
257
|
-
}));
|
|
258
|
-
}
|
|
206
|
+
if (value && Object.keys(value).length > 0 && JSON.stringify(value) !== JSON.stringify(capturedImages)) {
|
|
207
|
+
const updatedImages = value as Record<string, IIDCardPayload>;
|
|
208
|
+
setCapturedImages(updatedImages);
|
|
209
|
+
const currentImageData = updatedImages[currentSide];
|
|
210
|
+
if (currentImageData?.dir) {
|
|
211
|
+
setSilentCaptureResult(prev => ({ ...prev, path: currentImageData.dir, success: true, isAnalyzing: false, mrz: currentImageData.mrz || '', templatePath: currentImageData.templatePath || '' }));
|
|
259
212
|
}
|
|
260
213
|
}
|
|
261
214
|
}, [value, currentSide]);
|
|
262
215
|
|
|
263
|
-
// Keep refs in sync with state so handleSilentCapture always reads current values
|
|
264
|
-
useEffect(() => { isAnalyzingRef.current = silentCaptureResult.isAnalyzing ?? false; }, [silentCaptureResult.isAnalyzing]);
|
|
265
|
-
useEffect(() => { isProcessingCaptureRef.current = isProcessingCapture; }, [isProcessingCapture]);
|
|
266
|
-
useEffect(() => { scanPhaseRef.current = scanPhase; }, [scanPhase]);
|
|
267
|
-
|
|
268
216
|
const cameraConfig = useMemo(() => {
|
|
269
|
-
let instructions = selectedDocumentType
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (scanPhase === 'barcode') {
|
|
274
|
-
instructions = '';
|
|
275
|
-
}
|
|
276
|
-
|
|
217
|
+
let instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
218
|
+
if (scanPhase === 'barcode') instructions = '';
|
|
219
|
+
|
|
277
220
|
const isLandscape = screenWidth > screenHeight;
|
|
278
221
|
let xMin, xMax, yMin, yMax;
|
|
279
|
-
|
|
222
|
+
|
|
280
223
|
if (Platform.OS === 'web') {
|
|
281
224
|
const boxWidthPercent = isLandscape ? 0.40 : 0.85;
|
|
282
225
|
xMin = isLandscape ? 30 : 7.5;
|
|
283
226
|
xMax = xMin + (boxWidthPercent * 100);
|
|
284
|
-
|
|
285
227
|
const containerWidth = Math.min(screenWidth * 0.95, 1000);
|
|
286
228
|
const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
|
|
287
|
-
|
|
288
229
|
const boxPixelWidth = containerWidth * boxWidthPercent;
|
|
289
230
|
const boxPixelHeight = boxPixelWidth / 1.59;
|
|
290
|
-
|
|
291
231
|
const heightPercent = (boxPixelHeight / containerHeight) * 100;
|
|
292
232
|
yMin = (100 - heightPercent) / 2.5;
|
|
293
233
|
yMax = yMin + heightPercent;
|
|
294
234
|
} else {
|
|
295
|
-
xMin = 5;
|
|
296
|
-
xMax = 95;
|
|
297
|
-
yMin = 34;
|
|
298
|
-
yMax = 66;
|
|
235
|
+
xMin = 5; xMax = 95; yMin = 34; yMax = 66;
|
|
299
236
|
}
|
|
300
|
-
|
|
301
|
-
const platformBbox
|
|
302
|
-
xMin,
|
|
303
|
-
yMin,
|
|
304
|
-
xMax,
|
|
305
|
-
yMax,
|
|
306
|
-
borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60',
|
|
307
|
-
borderWidth: Platform.OS === 'web' ? 2 : 3,
|
|
308
|
-
cornerRadius: 8
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
313
|
-
flashMode: 'auto' as const,
|
|
314
|
-
overlay: { guideText: instructions, bbox: platformBbox }
|
|
315
|
-
};
|
|
237
|
+
const platformBbox = { xMin, yMin, xMax, yMax, borderColor: scanPhase === 'barcode' ? '#EAB308' : '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
|
|
238
|
+
return { cameraType: Platform.OS === 'web' ? cameraType : 'back', flashMode: 'auto' as const, overlay: { guideText: instructions, bbox: platformBbox } };
|
|
316
239
|
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight, scanPhase]);
|
|
317
240
|
|
|
318
241
|
const retakePicture = (sideToRetake: 'front' | 'back') => {
|
|
319
|
-
setIsProcessingCapture(false);
|
|
320
|
-
|
|
321
|
-
scannedBarcodeRef.current = null;
|
|
322
|
-
setScanPhase('image');
|
|
323
|
-
setPendingCAImage(null);
|
|
242
|
+
setIsProcessingCapture(false); setProcessingImagePath(null); scannedBarcodeRef.current = null;
|
|
243
|
+
setScanPhase('image'); setPendingCAImage(null);
|
|
324
244
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
325
|
-
setShowCamera(true);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
setCapturedImages((prev) => {
|
|
329
|
-
const newState = { ...prev };
|
|
330
|
-
delete newState[sideToRetake];
|
|
331
|
-
return newState;
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
if (value) {
|
|
335
|
-
const newValue = { ...value };
|
|
336
|
-
delete newValue[sideToRetake];
|
|
337
|
-
onValueChange(newValue);
|
|
338
|
-
}
|
|
245
|
+
setShowCamera(true); refreshCamera(); actions.showCustomStepper(false);
|
|
246
|
+
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
247
|
+
if (value) { const newValue = { ...value }; delete newValue[sideToRetake]; onValueChange(newValue); }
|
|
339
248
|
};
|
|
340
249
|
|
|
341
|
-
const getCurrentSideVerification = (
|
|
342
|
-
const rawDocType =
|
|
250
|
+
const getCurrentSideVerification = (side: string, countryKey: string, cData: any) => {
|
|
251
|
+
const rawDocType = cData?.documentType;
|
|
343
252
|
if (!rawDocType || !countryKey) return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
344
|
-
|
|
345
|
-
const jsonDocMap: Record<string, string> = {
|
|
346
|
-
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
347
|
-
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
348
|
-
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
349
|
-
};
|
|
350
|
-
|
|
253
|
+
|
|
254
|
+
const jsonDocMap: Record<string, string> = { 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card' };
|
|
351
255
|
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
352
|
-
const rawCountryName = ISO_TO_COUNTRY_NAME[
|
|
256
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[cData?.code || ''] || cData?.code || countryKey;
|
|
353
257
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
354
258
|
|
|
355
259
|
let countryMapping = baseMapping[rawCountryName];
|
|
@@ -357,22 +261,19 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
357
261
|
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
358
262
|
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
359
263
|
}
|
|
360
|
-
|
|
361
264
|
if (!countryMapping || !countryMapping[searchDocType]) return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
362
|
-
|
|
363
|
-
const authMethod: string[] = [];
|
|
364
|
-
const
|
|
365
|
-
const rawRegionKey = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
|
|
265
|
+
|
|
266
|
+
const authMethod: string[] = []; const mrzTypes: string[] = [];
|
|
267
|
+
const rawRegionKey = cData.region?.trim()?.length > 0 ? cData.region.trim() : 'root';
|
|
366
268
|
const key = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
367
269
|
const regionMapping = countryMapping[searchDocType];
|
|
368
|
-
|
|
270
|
+
|
|
369
271
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
370
272
|
regionMapping[key].forEach((item: any) => {
|
|
371
|
-
if (item[
|
|
273
|
+
if (item[side]) authMethod.push(item[side]);
|
|
372
274
|
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
373
275
|
});
|
|
374
276
|
}
|
|
375
|
-
|
|
376
277
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
377
278
|
}
|
|
378
279
|
|
|
@@ -397,119 +298,88 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
397
298
|
const autoCapture = async (capturePath: string, verified: ISilentCaptureResult & { region?: string }) => {
|
|
398
299
|
if (isProcessingCapture) return;
|
|
399
300
|
setIsProcessingCapture(true);
|
|
400
|
-
|
|
401
|
-
const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://'))
|
|
402
|
-
? `file://${capturePath}`
|
|
403
|
-
: capturePath;
|
|
404
|
-
|
|
301
|
+
const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://')) ? `file://${capturePath}` : capturePath;
|
|
405
302
|
setProcessingImagePath(safeCapturePath);
|
|
406
|
-
|
|
303
|
+
|
|
407
304
|
try {
|
|
408
305
|
let imagePathForUpload = capturePath;
|
|
409
306
|
const overlayBbox = cameraConfig.overlay.bbox;
|
|
410
|
-
const uiCropBbox = {
|
|
411
|
-
|
|
412
|
-
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
413
|
-
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
414
|
-
};
|
|
415
|
-
|
|
307
|
+
const uiCropBbox = { minX: overlayBbox.xMin / 100, minY: overlayBbox.yMin / 100, width: (overlayBbox.xMax - overlayBbox.xMin) / 100, height: (overlayBbox.yMax - overlayBbox.yMin) / 100 };
|
|
308
|
+
|
|
416
309
|
try {
|
|
417
310
|
imagePathForUpload = await cropImageWithBBoxWithTolerance(safeCapturePath, uiCropBbox, 0.06);
|
|
418
311
|
} catch (e) {
|
|
419
312
|
console.warn("Crop failed, falling back to original image", e);
|
|
420
|
-
imagePathForUpload = safeCapturePath;
|
|
313
|
+
imagePathForUpload = safeCapturePath;
|
|
421
314
|
}
|
|
422
|
-
|
|
423
|
-
const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://'))
|
|
424
|
-
? `file://${imagePathForUpload}`
|
|
425
|
-
: imagePathForUpload;
|
|
426
|
-
|
|
315
|
+
|
|
316
|
+
const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://')) ? `file://${imagePathForUpload}` : imagePathForUpload;
|
|
427
317
|
const base64 = await pathToBase64(safeCroppedPath);
|
|
318
|
+
const side = currentSideRef.current; // Grab live ref
|
|
319
|
+
|
|
428
320
|
const newImages = {
|
|
429
|
-
...
|
|
430
|
-
|
|
431
|
-
// Image source={{ uri }} never receives a bare path on iOS.
|
|
432
|
-
[currentSide]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
321
|
+
...capturedImagesRef.current,
|
|
322
|
+
[side]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
433
323
|
};
|
|
434
|
-
|
|
324
|
+
|
|
435
325
|
setCapturedImages(newImages);
|
|
436
|
-
|
|
437
326
|
if (verified.country && verified.documentType) {
|
|
438
327
|
const docType = verified.documentType as string;
|
|
439
328
|
const resolvedDocumentType = GovernmentDocumentTypeBackend[docType] || (Object.values(GovernmentDocumentTypeBackend).includes(docType as GovernmentDocumentType) ? docType : null) || Object.entries(GovernmentDocumentTypeBackend).find(([, v]) => v === docType)?.[1] || docType;
|
|
440
|
-
onValueChange({
|
|
441
|
-
...newImages,
|
|
442
|
-
country: verified.country,
|
|
443
|
-
documentType: resolvedDocumentType,
|
|
444
|
-
...(verified.region ? { region: verified.region } : {})
|
|
445
|
-
});
|
|
329
|
+
onValueChange({ ...newImages, country: verified.country, documentType: resolvedDocumentType, ...(verified.region ? { region: verified.region } : {}) });
|
|
446
330
|
}
|
|
447
|
-
|
|
331
|
+
|
|
448
332
|
setTimeout(() => {
|
|
449
|
-
setShowCamera(false);
|
|
450
|
-
actions.showCustomStepper(true);
|
|
333
|
+
setShowCamera(false); actions.showCustomStepper(true);
|
|
451
334
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
452
|
-
setIsProcessingCapture(false);
|
|
453
|
-
|
|
454
|
-
setScanPhase('image');
|
|
455
|
-
setPendingCAImage(null);
|
|
335
|
+
setIsProcessingCapture(false); setProcessingImagePath(null);
|
|
336
|
+
setScanPhase('image'); setPendingCAImage(null);
|
|
456
337
|
}, 600);
|
|
338
|
+
|
|
457
339
|
} catch (e: any) {
|
|
458
|
-
console.error("Backend Error:", e);
|
|
459
340
|
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
460
341
|
showAlert('Error', friendlyError);
|
|
461
342
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
462
|
-
setIsProcessingCapture(false);
|
|
463
|
-
setProcessingImagePath(null);
|
|
343
|
+
setIsProcessingCapture(false); setProcessingImagePath(null);
|
|
464
344
|
}
|
|
465
345
|
};
|
|
466
346
|
|
|
467
|
-
const isBarcodeRequired = (): boolean => {
|
|
468
|
-
if (
|
|
469
|
-
if (!
|
|
470
|
-
|
|
471
|
-
const rawDocType = selectedDocumentType.type;
|
|
472
|
-
const jsonDocMap: Record<string, string> = {
|
|
473
|
-
'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport',
|
|
474
|
-
'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
|
|
475
|
-
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card'
|
|
476
|
-
};
|
|
347
|
+
const isBarcodeRequired = (side: string, cData: any, docType: any): boolean => {
|
|
348
|
+
if (side !== 'back') return false;
|
|
349
|
+
if (!cData?.code || !docType?.type) return false;
|
|
477
350
|
|
|
351
|
+
const rawDocType = docType.type;
|
|
352
|
+
const jsonDocMap: Record<string, string> = { 'nationalId': 'identity_card', 'national_id': 'identity_card', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card' };
|
|
478
353
|
const searchDocType = jsonDocMap[rawDocType] || rawDocType;
|
|
479
|
-
const rawCountryName = ISO_TO_COUNTRY_NAME[
|
|
354
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[cData.code] || cData.code;
|
|
480
355
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
481
356
|
|
|
482
357
|
let countryMapping = baseMapping[rawCountryName];
|
|
483
358
|
if (!countryMapping) {
|
|
484
|
-
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() ===
|
|
359
|
+
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === cData.code.toLowerCase());
|
|
485
360
|
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
486
361
|
}
|
|
487
|
-
|
|
488
362
|
if (!countryMapping || !countryMapping[searchDocType]) return false;
|
|
489
|
-
|
|
490
|
-
const rawRegionKey =
|
|
363
|
+
|
|
364
|
+
const rawRegionKey = cData.region?.trim()?.length > 0 ? cData.region.trim() : 'root';
|
|
491
365
|
const regionKey = SHORT_TO_LONG_REGION[rawRegionKey] || rawRegionKey;
|
|
492
366
|
const regionData = countryMapping[searchDocType][regionKey];
|
|
493
367
|
|
|
494
368
|
if (!regionData || !Array.isArray(regionData)) return false;
|
|
495
|
-
|
|
496
|
-
const requiresBarcode = regionData.some((item: any) => {
|
|
369
|
+
return regionData.some((item: any) => {
|
|
497
370
|
const method = String(item['back'] || '').toUpperCase();
|
|
498
371
|
return method.includes('BARCODE') || method.includes('2D');
|
|
499
372
|
});
|
|
500
|
-
|
|
501
|
-
logger.log(`[Barcode Check] Dynamic check for ${countryData.code} - ${searchDocType} - ${regionKey}: requiresBarcode=${requiresBarcode}`);
|
|
502
|
-
return requiresBarcode;
|
|
503
373
|
};
|
|
504
374
|
|
|
505
375
|
const processCanadianBarcode = async (barcodeString: string) => {
|
|
506
|
-
if (
|
|
507
|
-
logger.log('[CA Barcode] Valid barcode decoded. Sending to parser...');
|
|
508
|
-
|
|
376
|
+
if (isProcessingCaptureRef.current || !pendingCAImage) return;
|
|
509
377
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
|
|
510
378
|
|
|
511
379
|
try {
|
|
512
|
-
const
|
|
380
|
+
const docType = selectedDocTypeRef.current;
|
|
381
|
+
const cData = countryDataRef.current;
|
|
382
|
+
const isHIC = docType!.type === 'health_insurance_card';
|
|
513
383
|
let parsedDictionary;
|
|
514
384
|
|
|
515
385
|
if (isHIC && barcodeString.length < 50) {
|
|
@@ -518,34 +388,22 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
518
388
|
const token = await authentification();
|
|
519
389
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeString, token }, env);
|
|
520
390
|
}
|
|
521
|
-
|
|
522
391
|
if (!parsedDictionary) throw new Error("Could not parse the barcode data.");
|
|
523
|
-
|
|
392
|
+
|
|
524
393
|
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
525
|
-
path: pendingCAImage.path,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
success: true,
|
|
529
|
-
mrz: JSON.stringify(parsedDictionary),
|
|
530
|
-
barcode: barcodeString,
|
|
531
|
-
isAnalyzing: false,
|
|
532
|
-
country: countryData?.code,
|
|
533
|
-
documentType: selectedDocumentType!.type,
|
|
534
|
-
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
394
|
+
path: pendingCAImage.path, templatePath: pendingCAImage.templatePath, bbox: pendingCAImage.bbox, success: true,
|
|
395
|
+
mrz: JSON.stringify(parsedDictionary), barcode: barcodeString, isAnalyzing: false,
|
|
396
|
+
country: cData?.code, documentType: docType!.type, region: docType!.region !== 'root' ? docType!.region : undefined,
|
|
535
397
|
};
|
|
536
|
-
|
|
398
|
+
|
|
537
399
|
setSilentCaptureResult(verifiedResult);
|
|
538
400
|
if (pendingCAImage.bbox) {
|
|
539
401
|
const safeBbox = pendingCAImage.bbox as IBbox;
|
|
540
402
|
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
541
403
|
}
|
|
542
|
-
|
|
543
404
|
await autoCapture(pendingCAImage.path, verifiedResult);
|
|
544
405
|
} catch (error: any) {
|
|
545
|
-
|
|
546
|
-
setScanPhase('image');
|
|
547
|
-
setPendingCAImage(null);
|
|
548
|
-
scannedBarcodeRef.current = null;
|
|
406
|
+
setScanPhase('image'); setPendingCAImage(null); scannedBarcodeRef.current = null;
|
|
549
407
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
|
|
550
408
|
}
|
|
551
409
|
};
|
|
@@ -554,47 +412,39 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
554
412
|
if (isProcessingCaptureRef.current) return;
|
|
555
413
|
if (scanPhaseRef.current === 'barcode') return;
|
|
556
414
|
if (isAnalyzingRef.current) return;
|
|
557
|
-
|
|
415
|
+
|
|
416
|
+
// USE REFS TO AVOID IOS NATIVE STALE CLOSURE
|
|
417
|
+
const side = currentSideRef.current;
|
|
418
|
+
const captured = capturedImagesRef.current;
|
|
419
|
+
const docType = selectedDocTypeRef.current;
|
|
420
|
+
const cData = countryDataRef.current;
|
|
421
|
+
|
|
558
422
|
if (result.success && result.path) {
|
|
559
423
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
560
|
-
|
|
424
|
+
|
|
561
425
|
const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
|
|
562
|
-
let templatePath =
|
|
426
|
+
let templatePath = captured[side]?.templatePath || '';
|
|
563
427
|
let templateBbox: IBbox | undefined;
|
|
564
428
|
let templateResponse: any;
|
|
565
|
-
|
|
566
|
-
if (!
|
|
429
|
+
|
|
430
|
+
if (!docType) {
|
|
567
431
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
568
432
|
return;
|
|
569
433
|
}
|
|
570
|
-
|
|
571
|
-
const issuingSubdivision =
|
|
572
|
-
|
|
573
|
-
// Subtle timeout handling without blocking pop-ups
|
|
434
|
+
|
|
435
|
+
const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
|
|
436
|
+
|
|
574
437
|
const watchdog = setTimeout(() => {
|
|
575
438
|
if (isAnalyzingRef.current) {
|
|
576
|
-
setSilentCaptureResult((prev) => ({
|
|
577
|
-
...prev,
|
|
578
|
-
isAnalyzing: false,
|
|
579
|
-
success: false,
|
|
580
|
-
error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.'
|
|
581
|
-
}));
|
|
439
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: state.currentLanguage === 'en' ? 'Taking longer than expected. Hold steady.' : 'Traitement long. Veuillez rester immobile.' }));
|
|
582
440
|
}
|
|
583
441
|
}, 20000);
|
|
584
|
-
|
|
442
|
+
|
|
585
443
|
try {
|
|
586
444
|
if (!templatePath) {
|
|
587
|
-
const templateType = await checkTemplateType({
|
|
588
|
-
path: safeCapturePath,
|
|
589
|
-
docType: selectedDocumentType?.type as GovernmentDocumentType,
|
|
590
|
-
docRegion: countryData?.code || "",
|
|
591
|
-
postfix: currentSide,
|
|
592
|
-
issuingSubdivision: issuingSubdivision
|
|
593
|
-
}, env);
|
|
594
|
-
|
|
445
|
+
const templateType = await checkTemplateType({ path: safeCapturePath, docType: docType.type as GovernmentDocumentType, docRegion: cData?.code || "", postfix: side, issuingSubdivision: issuingSubdivision }, env);
|
|
595
446
|
templateResponse = templateType;
|
|
596
447
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
597
|
-
|
|
598
448
|
if (templateType.card_obb) {
|
|
599
449
|
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
600
450
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
@@ -602,210 +452,108 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
602
452
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: t('kyc.idCardCapture.cardNotFullyInFrame') }));
|
|
603
453
|
return;
|
|
604
454
|
}
|
|
605
|
-
try {
|
|
606
|
-
const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
|
|
607
|
-
templateBbox = crop.bbox;
|
|
608
|
-
} catch {}
|
|
455
|
+
try { const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb); templateBbox = crop.bbox; } catch {}
|
|
609
456
|
}
|
|
610
457
|
}
|
|
611
|
-
|
|
612
|
-
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[
|
|
613
|
-
const regionMappings = getCurrentSideVerification(
|
|
458
|
+
|
|
459
|
+
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[cData?.code || ''] || 'root');
|
|
460
|
+
const regionMappings = getCurrentSideVerification(side, extractedCountryKey, cData);
|
|
614
461
|
let verificationRes: any;
|
|
615
|
-
|
|
616
|
-
if (
|
|
462
|
+
|
|
463
|
+
if (side === 'front') {
|
|
617
464
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
618
465
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
619
|
-
|
|
620
|
-
verificationRes = await frontVerification({
|
|
621
|
-
path: safeCapturePath,
|
|
622
|
-
regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] },
|
|
623
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
624
|
-
code: countryData?.code || '',
|
|
625
|
-
currentSide,
|
|
626
|
-
templatePath,
|
|
627
|
-
mrzType,
|
|
628
|
-
issuingSubdivision: issuingSubdivision,
|
|
629
|
-
clientSideBarcodeData: silentCaptureResult.barcode
|
|
630
|
-
}, env);
|
|
466
|
+
verificationRes = await frontVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : [], mrzTypes: mrzType ? [mrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType, issuingSubdivision: issuingSubdivision, clientSideBarcodeData: silentCaptureResult.barcode }, env);
|
|
631
467
|
} else {
|
|
632
|
-
logger.log("Attempting backVerification with:", { path: safeCapturePath, templatePath, side: currentSide });
|
|
633
468
|
const matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
634
469
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
635
|
-
const frontTemplatePath =
|
|
636
|
-
|
|
637
|
-
verificationRes = await backVerification({
|
|
638
|
-
path: safeCapturePath,
|
|
639
|
-
regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] },
|
|
640
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
641
|
-
code: countryData?.code || '',
|
|
642
|
-
currentSide,
|
|
643
|
-
templatePath,
|
|
644
|
-
mrzType: backMrzType,
|
|
645
|
-
templateResponse,
|
|
646
|
-
issuingSubdivision: issuingSubdivision,
|
|
647
|
-
frontTemplatePath: frontTemplatePath
|
|
648
|
-
}, env);
|
|
470
|
+
const frontTemplatePath = captured['front']?.templatePath || '';
|
|
471
|
+
verificationRes = await backVerification({ path: safeCapturePath, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : [], mrzTypes: backMrzType ? [backMrzType] : [] }, selectedDocumentType: GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: cData?.code || '', currentSide: side, templatePath, mrzType: backMrzType, templateResponse, issuingSubdivision: issuingSubdivision, frontTemplatePath: frontTemplatePath }, env);
|
|
649
472
|
}
|
|
650
|
-
|
|
473
|
+
|
|
651
474
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
652
475
|
const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
653
|
-
|
|
654
|
-
if (isBarcodeRequired()) {
|
|
476
|
+
|
|
477
|
+
if (isBarcodeRequired(side, cData, docType)) {
|
|
655
478
|
const barcodeData = scannedBarcodeRef.current;
|
|
656
|
-
const
|
|
657
|
-
const isHIC = docTypeStr === 'health_insurance_card';
|
|
479
|
+
const isHIC = docType.type === 'health_insurance_card';
|
|
658
480
|
const isValidScan = typeof barcodeData === 'string' && ((isHIC && barcodeData.length > 0) || barcodeData.length > 50);
|
|
659
|
-
|
|
481
|
+
|
|
660
482
|
if (isValidScan) {
|
|
661
|
-
logger.log('[CA Barcode] Image captured, verified & barcode ALREADY scanned! Skipping Phase 2.');
|
|
662
483
|
let parsedDictionary;
|
|
663
|
-
|
|
664
484
|
if (isHIC && barcodeData.length < 50) {
|
|
665
485
|
parsedDictionary = { data: { card_number: barcodeData, barcode_type: "1D" } };
|
|
666
486
|
} else {
|
|
667
487
|
const token = await authentification();
|
|
668
488
|
parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
|
|
669
489
|
}
|
|
670
|
-
|
|
671
|
-
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
672
|
-
path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
|
|
673
|
-
mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
|
|
674
|
-
country: countryData?.code, documentType: selectedDocumentType!.type,
|
|
675
|
-
region: selectedDocumentType!.region !== 'root' ? selectedDocumentType!.region : undefined,
|
|
676
|
-
};
|
|
490
|
+
const verifiedResult: ISilentCaptureResult & { region?: string } = { path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true, mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined };
|
|
677
491
|
|
|
678
|
-
clearTimeout(watchdog);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (bbox) {
|
|
682
|
-
const safeBbox = bbox as IBbox;
|
|
683
|
-
setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
|
|
684
|
-
}
|
|
492
|
+
clearTimeout(watchdog); setSilentCaptureResult(verifiedResult);
|
|
493
|
+
if (bbox) setBboxBySide(prev => ({ ...prev, ['back']: bbox as IBbox }));
|
|
685
494
|
await autoCapture(safeCapturePath!, verifiedResult);
|
|
686
495
|
return;
|
|
687
496
|
}
|
|
688
|
-
|
|
689
|
-
logger.log('[CA Barcode] Image captured & template VERIFIED. Switching to Barcode phase.');
|
|
497
|
+
|
|
690
498
|
clearTimeout(watchdog);
|
|
691
499
|
setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
|
|
692
500
|
setScanPhase('barcode');
|
|
693
501
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
694
502
|
return;
|
|
695
503
|
}
|
|
696
|
-
|
|
697
|
-
const verifiedResult: ISilentCaptureResult & { region?: string } = {
|
|
698
|
-
path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz,
|
|
699
|
-
isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type,
|
|
700
|
-
region: issuingSubdivision
|
|
701
|
-
};
|
|
702
|
-
|
|
504
|
+
|
|
505
|
+
const verifiedResult: ISilentCaptureResult & { region?: string } = { path: safeCapturePath, templatePath, bbox, success: true, mrz: verifiedMrz, isAnalyzing: false, country: cData?.code, documentType: docType.type, region: issuingSubdivision };
|
|
703
506
|
clearTimeout(watchdog);
|
|
704
507
|
setSilentCaptureResult(verifiedResult);
|
|
705
|
-
if (bbox) setBboxBySide(prev => ({ ...prev, [
|
|
508
|
+
if (bbox) setBboxBySide(prev => ({ ...prev, [side]: bbox }));
|
|
706
509
|
await autoCapture(safeCapturePath, verifiedResult);
|
|
707
|
-
|
|
510
|
+
|
|
708
511
|
} catch (error: any) {
|
|
709
512
|
clearTimeout(watchdog);
|
|
710
513
|
const isNetworkError = !error.response || error.message === 'Network Error' || error.code === 'ECONNABORTED' || error.message?.includes('Network');
|
|
711
|
-
|
|
712
|
-
// Keep UI errors extremely brief and actionable
|
|
713
514
|
let hintMessage = '';
|
|
714
|
-
|
|
715
515
|
if (isNetworkError) {
|
|
716
516
|
hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Retrying...' : 'Connexion faible. Nouvelle tentative...';
|
|
717
517
|
} else {
|
|
718
518
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
719
|
-
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
} else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
724
|
-
hintMessage = state.currentLanguage === 'en' ? 'Move closer.' : 'Rapprochez-vous.';
|
|
725
|
-
} else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
726
|
-
hintMessage = state.currentLanguage === 'en' ? 'Scan the same ID.' : 'Scannez la même pièce.';
|
|
727
|
-
}
|
|
519
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) { hintMessage = state.currentLanguage === 'en' ? 'Move to better lighting.' : 'Améliorez l\'éclairage.'; }
|
|
520
|
+
else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) { hintMessage = state.currentLanguage === 'en' ? 'Wrong document type.' : 'Mauvais type de document.'; }
|
|
521
|
+
else if (rawMessage.includes('card_not_fully_in_frame')) { hintMessage = state.currentLanguage === 'en' ? 'Move closer.' : 'Rapprochez-vous.'; }
|
|
522
|
+
else if (rawMessage.includes('ne correspond pas au recto')) { hintMessage = state.currentLanguage === 'en' ? 'Scan the same ID.' : 'Scannez la même pièce.'; }
|
|
728
523
|
}
|
|
729
|
-
|
|
730
524
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: hintMessage }));
|
|
731
525
|
}
|
|
732
526
|
}
|
|
733
527
|
};
|
|
734
|
-
|
|
528
|
+
|
|
735
529
|
useEffect(() => {
|
|
736
530
|
if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
|
|
737
531
|
const bData = silentCaptureResult.barcode;
|
|
738
532
|
if (typeof bData === 'string' && bData.length > 50) {
|
|
739
|
-
logger.log('[CA Barcode] Safety Net Triggered: Processing barcode.');
|
|
740
533
|
processCanadianBarcode(bData);
|
|
741
534
|
}
|
|
742
535
|
}
|
|
743
536
|
}, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
|
|
744
|
-
|
|
745
537
|
|
|
746
538
|
const handleError = (event: { message: string, name?: string, code?: string }) => {
|
|
747
|
-
logger.error("Camera Error:", event);
|
|
748
539
|
const errString = `${event.message} ${event.name} ${event.code}`.toLowerCase();
|
|
749
|
-
|
|
750
540
|
let friendlyMessage = state.currentLanguage === 'en' ? 'Camera error.' : 'Erreur de caméra.';
|
|
751
|
-
|
|
752
|
-
if (errString.includes('
|
|
753
|
-
|
|
754
|
-
} else if (errString.includes('notallowederror') || errString.includes('permission')) {
|
|
755
|
-
friendlyMessage = state.currentLanguage === 'en' ? 'Allow camera access in settings.' : 'Autorisez l\'accès à la caméra.';
|
|
756
|
-
} else if (errString.includes('notfounderror') || errString.includes('no camera')) {
|
|
757
|
-
friendlyMessage = state.currentLanguage === 'en' ? 'No camera detected.' : 'Aucune caméra détectée.';
|
|
758
|
-
}
|
|
759
|
-
|
|
541
|
+
if (errString.includes('notreadableerror') || errString.includes('trackstarterror') || errString.includes('in use') || errString.includes('concurrent') || errString.includes('camera unavailable')) { friendlyMessage = state.currentLanguage === 'en' ? 'Camera is in use by another app.' : 'Caméra utilisée par une autre application.'; }
|
|
542
|
+
else if (errString.includes('notallowederror') || errString.includes('permission')) { friendlyMessage = state.currentLanguage === 'en' ? 'Allow camera access in settings.' : 'Autorisez l\'accès à la caméra.'; }
|
|
543
|
+
else if (errString.includes('notfounderror') || errString.includes('no camera')) { friendlyMessage = state.currentLanguage === 'en' ? 'No camera detected.' : 'Aucune caméra détectée.'; }
|
|
760
544
|
showAlert(state.currentLanguage === 'en' ? 'Camera Error' : 'Erreur', friendlyMessage);
|
|
761
|
-
setShowCamera(false);
|
|
762
|
-
setIsProcessingCapture(false);
|
|
545
|
+
setShowCamera(false); setIsProcessingCapture(false);
|
|
763
546
|
};
|
|
764
547
|
|
|
765
|
-
useEffect(() => {
|
|
766
|
-
actions.showCustomStepper(!showCamera);
|
|
767
|
-
}, [showCamera]);
|
|
768
|
-
|
|
769
|
-
useEffect(() => {
|
|
770
|
-
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
771
|
-
try {
|
|
772
|
-
const url = new URL(window.location.origin);
|
|
773
|
-
if (url.hostname === 'localhost') {
|
|
774
|
-
url.hostname = '192.168.0.33';
|
|
775
|
-
}
|
|
776
|
-
const params = new URLSearchParams(window.location.search);
|
|
777
|
-
params.set('session_id', state.session.session_id);
|
|
778
|
-
params.set('resume', 'true');
|
|
779
|
-
|
|
780
|
-
if (state.currentComponentIndex !== undefined) {
|
|
781
|
-
params.set('component_index', String(state.currentComponentIndex));
|
|
782
|
-
}
|
|
783
|
-
if (countryData?.code) {
|
|
784
|
-
params.set('country', countryData.code);
|
|
785
|
-
}
|
|
786
|
-
if (selectedDocumentType?.type) {
|
|
787
|
-
params.set('document_type', selectedDocumentType.type);
|
|
788
|
-
}
|
|
789
|
-
if (selectedDocumentType?.region && selectedDocumentType.region !== 'root') {
|
|
790
|
-
params.set('region', selectedDocumentType.region);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
url.hash = params.toString();
|
|
794
|
-
setCurrentUrl(url.toString());
|
|
795
|
-
} catch (e) {
|
|
796
|
-
console.error("Failed to construct QR handoff URL", e);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}, [state.session.session_id, state.currentComponentIndex, countryData, selectedDocumentType]);
|
|
548
|
+
useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
|
|
800
549
|
|
|
550
|
+
// Render logic remains identical below...
|
|
801
551
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
802
552
|
return (
|
|
803
553
|
<View style={styles.root}>
|
|
804
554
|
<View style={styles.previewContainer}>
|
|
805
555
|
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
806
|
-
<Text style={styles.description}>
|
|
807
|
-
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
808
|
-
</Text>
|
|
556
|
+
<Text style={styles.description}>{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}</Text>
|
|
809
557
|
</View>
|
|
810
558
|
</View>
|
|
811
559
|
);
|
|
@@ -813,7 +561,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
813
561
|
|
|
814
562
|
if (showCamera) {
|
|
815
563
|
const isBusy = isProcessingCapture;
|
|
816
|
-
|
|
817
564
|
if (isRebootingCamera) {
|
|
818
565
|
return (
|
|
819
566
|
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
@@ -822,46 +569,30 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
822
569
|
</View>
|
|
823
570
|
);
|
|
824
571
|
}
|
|
825
|
-
|
|
826
572
|
return (
|
|
827
573
|
<View style={styles.root}>
|
|
828
574
|
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
829
575
|
<View style={styles.headerContainer}>
|
|
830
|
-
<Text style={styles.headerTitle}>
|
|
831
|
-
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
832
|
-
</Text>
|
|
576
|
+
<Text style={styles.headerTitle}>{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}</Text>
|
|
833
577
|
<View style={styles.stepBadge}>
|
|
834
|
-
<Text style={styles.stepText}>
|
|
835
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
836
|
-
</Text>
|
|
578
|
+
<Text style={styles.stepText}>{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}</Text>
|
|
837
579
|
</View>
|
|
838
580
|
</View>
|
|
839
|
-
|
|
840
581
|
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
841
582
|
{Platform.OS === 'web' && (
|
|
842
583
|
<View style={styles.webTopControls}>
|
|
843
|
-
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
844
|
-
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
845
|
-
</TouchableOpacity>
|
|
584
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}><Text style={styles.webFlipText}>🔄 Flip Lens</Text></TouchableOpacity>
|
|
846
585
|
</View>
|
|
847
586
|
)}
|
|
848
|
-
|
|
849
587
|
<EnhancedCameraView
|
|
850
|
-
key={`${currentSide}-${cameraKey}`}
|
|
851
|
-
showCamera={true}
|
|
852
|
-
isProcessing={isBusy}
|
|
853
|
-
cameraType={cameraConfig.cameraType}
|
|
588
|
+
key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType}
|
|
854
589
|
style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } as any : {}) }}
|
|
855
|
-
onError={handleError}
|
|
856
|
-
|
|
857
|
-
silentCaptureResult={silentCaptureResult}
|
|
858
|
-
isScanningBarcode={currentSide === 'back'}
|
|
859
|
-
barcodeTypes={targetBarcodeTypes}
|
|
590
|
+
onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult}
|
|
591
|
+
isScanningBarcode={currentSide === 'back'} barcodeTypes={targetBarcodeTypes}
|
|
860
592
|
onBarcodeScanned={(barcodeString) => {
|
|
861
593
|
const docTypeStr = selectedDocumentType?.type;
|
|
862
594
|
const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
|
|
863
595
|
if (isFullDataDoc && barcodeString.length < 50) return;
|
|
864
|
-
|
|
865
596
|
if (scannedBarcodeRef.current !== barcodeString) {
|
|
866
597
|
scannedBarcodeRef.current = barcodeString;
|
|
867
598
|
if (scanPhase === 'barcode' && pendingCAImage) {
|
|
@@ -872,122 +603,70 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
872
603
|
}
|
|
873
604
|
}}
|
|
874
605
|
overlayComponent={
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
stepperProps={{
|
|
886
|
-
back: () => {
|
|
887
|
-
if (currentSide === 'back') {
|
|
888
|
-
scannedBarcodeRef.current = null;
|
|
889
|
-
setScanPhase('image');
|
|
890
|
-
setPendingCAImage(null);
|
|
891
|
-
setCurrentSide('front');
|
|
892
|
-
setShowCamera(false);
|
|
893
|
-
setIsProcessingCapture(false);
|
|
894
|
-
setProcessingImagePath(null);
|
|
895
|
-
if (capturedImages['front']?.dir) {
|
|
896
|
-
const frontImage = capturedImages['front'];
|
|
897
|
-
setSilentCaptureResult((prev) => ({
|
|
898
|
-
...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front']
|
|
899
|
-
}));
|
|
900
|
-
} else {
|
|
901
|
-
setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
|
|
902
|
-
}
|
|
606
|
+
<IdCardOverlay
|
|
607
|
+
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
608
|
+
instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number} isSuccess={silentCaptureResult.success || scanPhase === 'barcode'} language={state.currentLanguage}
|
|
609
|
+
stepperProps={{
|
|
610
|
+
back: () => {
|
|
611
|
+
if (currentSide === 'back') {
|
|
612
|
+
scannedBarcodeRef.current = null; setScanPhase('image'); setPendingCAImage(null); setCurrentSide('front'); setShowCamera(false); setIsProcessingCapture(false); setProcessingImagePath(null);
|
|
613
|
+
if (capturedImages['front']?.dir) {
|
|
614
|
+
const frontImage = capturedImages['front'];
|
|
615
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
903
616
|
} else {
|
|
904
|
-
|
|
617
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
|
|
905
618
|
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}}
|
|
912
|
-
/>
|
|
913
|
-
</>
|
|
619
|
+
} else { actions.previousComponent(); }
|
|
620
|
+
},
|
|
621
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '', step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
622
|
+
}}
|
|
623
|
+
/>
|
|
914
624
|
}
|
|
915
625
|
/>
|
|
916
|
-
|
|
917
626
|
{scanPhase === 'barcode' && !isBusy && (
|
|
918
627
|
<View style={styles.barcodePromptContainer}>
|
|
919
628
|
<View style={styles.barcodePromptContent}>
|
|
920
|
-
<View style={styles.barcodeIconContainer}>
|
|
921
|
-
<Text style={styles.barcodeIcon}>✓</Text>
|
|
922
|
-
</View>
|
|
629
|
+
<View style={styles.barcodeIconContainer}><Text style={styles.barcodeIcon}>✓</Text></View>
|
|
923
630
|
<View style={styles.barcodeTextContainer}>
|
|
924
|
-
<Text style={styles.barcodePromptTitle}>
|
|
925
|
-
|
|
926
|
-
</Text>
|
|
927
|
-
<Text style={styles.barcodePromptDesc}>
|
|
928
|
-
{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}
|
|
929
|
-
</Text>
|
|
631
|
+
<Text style={styles.barcodePromptTitle}>{state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}</Text>
|
|
632
|
+
<Text style={styles.barcodePromptDesc}>{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}</Text>
|
|
930
633
|
</View>
|
|
931
634
|
</View>
|
|
932
635
|
</View>
|
|
933
636
|
)}
|
|
934
|
-
|
|
935
637
|
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
936
638
|
<View style={styles.topAnalyzingPillContainer}>
|
|
937
639
|
<View style={styles.topAnalyzingPill}>
|
|
938
640
|
<ActivityIndicator size="small" color="white" />
|
|
939
|
-
<Text style={styles.analyzingPillText}>
|
|
940
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
941
|
-
</Text>
|
|
641
|
+
<Text style={styles.analyzingPillText}>{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}</Text>
|
|
942
642
|
</View>
|
|
943
643
|
</View>
|
|
944
644
|
)}
|
|
945
|
-
|
|
946
645
|
{isBusy && (
|
|
947
646
|
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
948
|
-
{processingImagePath && (
|
|
949
|
-
<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover" />
|
|
950
|
-
)}
|
|
647
|
+
{processingImagePath && <Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover" />}
|
|
951
648
|
<View style={styles.processingOverlay}>
|
|
952
649
|
<ActivityIndicator size="large" color="#2DBD60" />
|
|
953
|
-
<Text style={styles.processingText}>
|
|
954
|
-
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
955
|
-
</Text>
|
|
650
|
+
<Text style={styles.processingText}>{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}</Text>
|
|
956
651
|
</View>
|
|
957
652
|
</View>
|
|
958
653
|
)}
|
|
959
|
-
|
|
960
654
|
{!isBusy && Platform.OS === 'web' ? (
|
|
961
655
|
<View style={styles.webBottomControlBar}>
|
|
962
|
-
{silentCaptureResult.error &&
|
|
963
|
-
<View style={styles.floatingErrorBanner}>
|
|
964
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
965
|
-
</View>
|
|
966
|
-
)}
|
|
656
|
+
{silentCaptureResult.error && <View style={styles.floatingErrorBanner}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>}
|
|
967
657
|
<View style={styles.webActionButtonsRow}>
|
|
968
|
-
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
969
|
-
|
|
970
|
-
</TouchableOpacity>
|
|
971
|
-
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
972
|
-
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
973
|
-
</TouchableOpacity>
|
|
658
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
|
|
659
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}><Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text></TouchableOpacity>
|
|
974
660
|
</View>
|
|
975
661
|
</View>
|
|
976
662
|
) : !isBusy ? (
|
|
977
663
|
<View style={styles.escapeHatchContainer}>
|
|
978
|
-
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
979
|
-
|
|
980
|
-
</TouchableOpacity>
|
|
981
|
-
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
982
|
-
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
983
|
-
</TouchableOpacity>
|
|
664
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}><Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text></TouchableOpacity>
|
|
665
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}><Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text></TouchableOpacity>
|
|
984
666
|
</View>
|
|
985
667
|
) : null}
|
|
986
|
-
|
|
987
668
|
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
|
|
988
|
-
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
989
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
990
|
-
</View>
|
|
669
|
+
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>
|
|
991
670
|
) : null}
|
|
992
671
|
</View>
|
|
993
672
|
</View>
|
|
@@ -1000,96 +679,36 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
1000
679
|
<View style={styles.previewContainer}>
|
|
1001
680
|
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
1002
681
|
<View key={currentSide} style={styles.sideContainer}>
|
|
1003
|
-
<Text style={styles.sideTitle}>
|
|
1004
|
-
|
|
1005
|
-
</Text>
|
|
1006
|
-
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
1007
|
-
{getLocalizedText(component.instructions)}
|
|
1008
|
-
</Text>
|
|
1009
|
-
|
|
682
|
+
<Text style={styles.sideTitle}>{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}</Text>
|
|
683
|
+
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>{getLocalizedText(component.instructions)}</Text>
|
|
1010
684
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
1011
685
|
<View style={styles.imagePreviewWrapper}>
|
|
1012
|
-
{capturedImages[currentSide]?.dir ? (
|
|
1013
|
-
<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage} />
|
|
1014
|
-
) : silentCaptureResult.path ? (
|
|
1015
|
-
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
1016
|
-
) : null}
|
|
686
|
+
{capturedImages[currentSide]?.dir ? ( <Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage} /> ) : silentCaptureResult.path ? ( <Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} /> ) : null}
|
|
1017
687
|
</View>
|
|
1018
|
-
|
|
1019
688
|
{!capturedImages[currentSide]?.dir && (
|
|
1020
689
|
<View style={{ width: '100%', gap: 12 }}>
|
|
1021
|
-
|
|
1022
690
|
{isWebScanningRestricted ? (
|
|
1023
691
|
<>
|
|
1024
692
|
<View style={styles.restrictionWarningBanner}>
|
|
1025
|
-
<Text style={styles.restrictionWarningTitle}>
|
|
1026
|
-
|
|
1027
|
-
</Text>
|
|
1028
|
-
<Text style={styles.restrictionWarningText}>
|
|
1029
|
-
{state.currentLanguage === 'en'
|
|
1030
|
-
? `This document can only be scanned using our mobile app.`
|
|
1031
|
-
: `Ce document ne peut être scanné qu'avec notre application mobile.`}
|
|
1032
|
-
</Text>
|
|
693
|
+
<Text style={styles.restrictionWarningTitle}>{state.currentLanguage === 'en' ? '⚠️ Web Scanning Unavailable' : '⚠️ Numérisation Web Indisponible'}</Text>
|
|
694
|
+
<Text style={styles.restrictionWarningText}>{state.currentLanguage === 'en' ? `This document can only be scanned using our mobile app.` : `Ce document ne peut être scanné qu'avec notre application mobile.`}</Text>
|
|
1033
695
|
</View>
|
|
1034
|
-
|
|
1035
|
-
<Button
|
|
1036
|
-
title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"}
|
|
1037
|
-
onPress={() => setShowQRModal(true)}
|
|
1038
|
-
variant="primary"
|
|
1039
|
-
size="large"
|
|
1040
|
-
fullWidth
|
|
1041
|
-
/>
|
|
696
|
+
<Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="primary" size="large" fullWidth />
|
|
1042
697
|
</>
|
|
1043
698
|
) : (
|
|
1044
|
-
/* 🟢 NORMAL FLOW: Show "Start Scanning" and optional web handoff */
|
|
1045
699
|
<>
|
|
1046
|
-
<Button
|
|
1047
|
-
|
|
1048
|
-
onPress={() => {
|
|
1049
|
-
setShowCamera(true);
|
|
1050
|
-
refreshCamera();
|
|
1051
|
-
actions.showCustomStepper(false);
|
|
1052
|
-
}}
|
|
1053
|
-
variant="primary"
|
|
1054
|
-
size="large"
|
|
1055
|
-
fullWidth
|
|
1056
|
-
/>
|
|
1057
|
-
|
|
1058
|
-
{Platform.OS === 'web' && (
|
|
1059
|
-
<Button
|
|
1060
|
-
title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"}
|
|
1061
|
-
onPress={() => setShowQRModal(true)}
|
|
1062
|
-
variant="outline"
|
|
1063
|
-
size="large"
|
|
1064
|
-
fullWidth
|
|
1065
|
-
/>
|
|
1066
|
-
)}
|
|
700
|
+
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth />
|
|
701
|
+
{Platform.OS === 'web' && ( <Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="outline" size="large" fullWidth /> )}
|
|
1067
702
|
</>
|
|
1068
703
|
)}
|
|
1069
|
-
|
|
1070
704
|
</View>
|
|
1071
705
|
)}
|
|
1072
|
-
|
|
1073
706
|
{capturedImages[currentSide]?.dir && (
|
|
1074
707
|
<>
|
|
1075
708
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth />
|
|
1076
709
|
<Button title={t('common.next')} onPress={() => {
|
|
1077
|
-
if (!selectedDocumentType) {
|
|
1078
|
-
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
if (currentSide === 'back' || selectedDocumentType.type === 'passport') {
|
|
1082
|
-
actions.nextComponent();
|
|
1083
|
-
} else {
|
|
1084
|
-
setShowCamera(true);
|
|
1085
|
-
refreshCamera();
|
|
1086
|
-
setCurrentSide('back');
|
|
1087
|
-
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
1088
|
-
setIsProcessingCapture(false);
|
|
1089
|
-
setProcessingImagePath(null);
|
|
1090
|
-
setScanPhase('image');
|
|
1091
|
-
setPendingCAImage(null);
|
|
1092
|
-
}
|
|
710
|
+
if (!selectedDocumentType) { showAlert('Error', 'Document type not selected'); return; }
|
|
711
|
+
if (currentSide === 'back' || selectedDocumentType.type === 'passport') { actions.nextComponent(); } else { setShowCamera(true); refreshCamera(); setCurrentSide('back'); setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined }); setIsProcessingCapture(false); setProcessingImagePath(null); setScanPhase('image'); setPendingCAImage(null); }
|
|
1093
712
|
}} variant="primary" size="large" fullWidth />
|
|
1094
713
|
</>
|
|
1095
714
|
)}
|
|
@@ -1097,98 +716,98 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
1097
716
|
</View>
|
|
1098
717
|
</ScrollView>
|
|
1099
718
|
</View>
|
|
1100
|
-
|
|
719
|
+
|
|
1101
720
|
{showQRModal && Platform.OS === 'web' && (
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
721
|
+
<View style={styles.qrModalOverlay}>
|
|
722
|
+
<View style={styles.qrModalContainer}>
|
|
723
|
+
<View style={styles.qrModalHeader}>
|
|
724
|
+
<Text style={styles.qrModalTitle}>
|
|
725
|
+
{state.currentLanguage === 'en' ? 'Continue on your phone' : 'Continuer sur votre téléphone'}
|
|
726
|
+
</Text>
|
|
727
|
+
<Text style={styles.qrModalText}>
|
|
728
|
+
{state.currentLanguage === 'en'
|
|
729
|
+
? "Scan the QR code to download our mobile app and complete the scanning process securely."
|
|
730
|
+
: "Scannez le code QR pour télécharger notre application et terminer la numérisation en toute sécurité."}
|
|
731
|
+
</Text>
|
|
732
|
+
</View>
|
|
1114
733
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
734
|
+
<View style={styles.qrToggleContainer}>
|
|
735
|
+
<TouchableOpacity
|
|
736
|
+
style={[styles.qrToggleButton, qrPlatform === 'android' && styles.qrToggleButtonActive]}
|
|
737
|
+
onPress={() => setQrPlatform('android')}
|
|
738
|
+
>
|
|
739
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'android' && styles.qrToggleTextActive]}>
|
|
740
|
+
Android
|
|
741
|
+
</Text>
|
|
742
|
+
</TouchableOpacity>
|
|
743
|
+
<TouchableOpacity
|
|
744
|
+
style={[styles.qrToggleButton, qrPlatform === 'ios' && styles.qrToggleButtonActive]}
|
|
745
|
+
onPress={() => setQrPlatform('ios')}
|
|
746
|
+
>
|
|
747
|
+
<Text style={[styles.qrToggleText, qrPlatform === 'ios' && styles.qrToggleTextActive]}>
|
|
748
|
+
iOS (Apple)
|
|
749
|
+
</Text>
|
|
750
|
+
</TouchableOpacity>
|
|
751
|
+
</View>
|
|
1133
752
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
753
|
+
<View style={styles.qrCodeWrapper}>
|
|
754
|
+
<QRCode
|
|
755
|
+
value={
|
|
756
|
+
qrPlatform === 'android'
|
|
757
|
+
? 'https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US'
|
|
758
|
+
: 'https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731'
|
|
759
|
+
}
|
|
760
|
+
size={180}
|
|
761
|
+
backgroundColor="#FFFFFF"
|
|
762
|
+
color="#0F172A"
|
|
763
|
+
/>
|
|
764
|
+
</View>
|
|
1146
765
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
</Text>
|
|
1151
|
-
|
|
1152
|
-
{generatedCode ? (
|
|
1153
|
-
<ExpiringCodeTimer initialCode={generatedCode} language={state.currentLanguage} />
|
|
1154
|
-
) : (
|
|
1155
|
-
<TouchableOpacity style={styles.generateCodeButton} onPress={handleGenerateCode} disabled={isGeneratingCode}>
|
|
1156
|
-
{isGeneratingCode ? (
|
|
1157
|
-
<ActivityIndicator size="small" color="#0F172A" />
|
|
1158
|
-
) : (
|
|
1159
|
-
<Text style={styles.generateCodeButtonText}>
|
|
1160
|
-
{state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
|
|
766
|
+
<View style={styles.linkCodeSection}>
|
|
767
|
+
<Text style={styles.linkCodeHeader}>
|
|
768
|
+
{state.currentLanguage === 'en' ? 'Use code below to proceed on the Sanctum Key App.' : 'Vous avez déjà l\'application ? Liez-la :'}
|
|
1161
769
|
</Text>
|
|
770
|
+
|
|
771
|
+
{generatedCode ? (
|
|
772
|
+
<ExpiringCodeTimer initialCode={generatedCode} language={state.currentLanguage} />
|
|
773
|
+
) : (
|
|
774
|
+
<TouchableOpacity style={styles.generateCodeButton} onPress={handleGenerateCode} disabled={isGeneratingCode}>
|
|
775
|
+
{isGeneratingCode ? (
|
|
776
|
+
<ActivityIndicator size="small" color="#0F172A" />
|
|
777
|
+
) : (
|
|
778
|
+
<Text style={styles.generateCodeButtonText}>
|
|
779
|
+
{state.currentLanguage === 'en' ? 'Generate 5-Digit Link Code' : 'Générer un code à 5 chiffres'}
|
|
780
|
+
</Text>
|
|
781
|
+
)}
|
|
782
|
+
</TouchableOpacity>
|
|
783
|
+
)}
|
|
784
|
+
</View>
|
|
785
|
+
|
|
786
|
+
{isWebScanningRestricted && (
|
|
787
|
+
<View style={styles.storeLinksContainer}>
|
|
788
|
+
<Text style={styles.storeLinksTitle}>
|
|
789
|
+
{state.currentLanguage === 'en' ? 'Or click to open directly:' : 'Ou cliquez pour ouvrir directement :'}
|
|
790
|
+
</Text>
|
|
791
|
+
<View style={styles.storeButtonsRow}>
|
|
792
|
+
<TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://play.google.com/store/apps/details?id=transfergratis.com&hl=en_CA&gl=US', '_blank')} >
|
|
793
|
+
<Text style={styles.storeButtonText}>Google Play</Text>
|
|
794
|
+
</TouchableOpacity>
|
|
795
|
+
<TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')} >
|
|
796
|
+
<Text style={styles.storeButtonText}>App Store</Text>
|
|
797
|
+
</TouchableOpacity>
|
|
798
|
+
</View>
|
|
799
|
+
</View>
|
|
1162
800
|
)}
|
|
1163
|
-
</TouchableOpacity>
|
|
1164
|
-
)}
|
|
1165
|
-
</View>
|
|
1166
801
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
<Text style={styles.storeButtonText}>Google Play</Text>
|
|
1175
|
-
</TouchableOpacity>
|
|
1176
|
-
<TouchableOpacity style={styles.storeButton} onPress={() => window.open('https://apps.apple.com/ca/app/transfergratis-money-transfer/id1605411731', '_blank')} >
|
|
1177
|
-
<Text style={styles.storeButtonText}>App Store</Text>
|
|
1178
|
-
</TouchableOpacity>
|
|
802
|
+
<Button
|
|
803
|
+
title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"}
|
|
804
|
+
onPress={() => setShowQRModal(false)}
|
|
805
|
+
variant="outline"
|
|
806
|
+
fullWidth
|
|
807
|
+
/>
|
|
808
|
+
</View>
|
|
1179
809
|
</View>
|
|
1180
|
-
|
|
1181
|
-
)}
|
|
1182
|
-
|
|
1183
|
-
<Button
|
|
1184
|
-
title={state.currentLanguage === 'en' ? "Cancel & Stay on Laptop" : "Annuler et rester sur l'ordinateur"}
|
|
1185
|
-
onPress={() => setShowQRModal(false)}
|
|
1186
|
-
variant="outline"
|
|
1187
|
-
fullWidth
|
|
1188
|
-
/>
|
|
1189
|
-
</View>
|
|
1190
|
-
</View>
|
|
1191
|
-
)}
|
|
810
|
+
)}
|
|
1192
811
|
</View>
|
|
1193
812
|
);
|
|
1194
813
|
};
|