@sanctum-key/react-native-sdk 1.0.11 → 1.0.12
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 +129 -181
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +226 -296
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native'; // 🚨 Added TouchableOpacity
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -14,21 +14,10 @@ import pathToBase64 from '../../utils/pathToBase64';
|
|
|
14
14
|
import { cropByObb, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
|
|
15
15
|
import REGION_MAPPING from '../../config/region_mapping.json';
|
|
16
16
|
|
|
17
|
-
const ISO_TO_COUNTRY_NAME: Record<string, string> = {
|
|
18
|
-
'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
|
|
19
|
-
'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
|
|
20
|
-
'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
|
|
21
|
-
};
|
|
17
|
+
const ISO_TO_COUNTRY_NAME: Record<string, string> = { 'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada', 'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain', 'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali' };
|
|
22
18
|
|
|
23
19
|
interface IIDCardPayload { dir: string; file: string; mrz: string; templatePath?: string; }
|
|
24
|
-
interface IDCardCaptureProps {
|
|
25
|
-
component: TemplateComponent;
|
|
26
|
-
value?: Record<string, IIDCardPayload>;
|
|
27
|
-
onValueChange: (value: Record<string, IIDCardPayload | string>) => void;
|
|
28
|
-
error?: string;
|
|
29
|
-
language?: string;
|
|
30
|
-
currentSide?: string;
|
|
31
|
-
}
|
|
20
|
+
interface IDCardCaptureProps { component: TemplateComponent; value?: Record<string, IIDCardPayload>; onValueChange: (value: Record<string, IIDCardPayload | string>) => void; error?: string; language?: string; currentSide?: string; }
|
|
32
21
|
|
|
33
22
|
export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
|
|
34
23
|
const { t, locale } = useI18n();
|
|
@@ -39,14 +28,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
39
28
|
const [silentCaptureResult, setSilentCaptureResult] = useState<ISilentCaptureResult>({ success: false, isAnalyzing: false });
|
|
40
29
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
41
30
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
// 🚨 ADDED: Key to force camera re-mount
|
|
42
33
|
const [cameraKey, setCameraKey] = useState(0);
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
const documentTypeMapping: Record<string, GovernmentDocumentType> = {
|
|
46
|
-
'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence',
|
|
47
|
-
'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card',
|
|
48
|
-
};
|
|
49
|
-
|
|
35
|
+
const documentTypeMapping: Record<string, GovernmentDocumentType> = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
|
|
50
36
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
51
37
|
|
|
52
38
|
const getLocalizedText = (text: LocalizedText | Record<string, LocalizedText>): string => {
|
|
@@ -54,10 +40,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
54
40
|
return "";
|
|
55
41
|
};
|
|
56
42
|
|
|
57
|
-
const refreshCamera = () => {
|
|
58
|
-
setCameraKey(prev => prev + 1);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
43
|
const countrySelectionData = useMemo(() => {
|
|
62
44
|
const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
|
|
63
45
|
return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
|
|
@@ -72,21 +54,28 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
72
54
|
|
|
73
55
|
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
74
56
|
|
|
57
|
+
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
58
|
+
|
|
59
|
+
const refreshCamera = () => {
|
|
60
|
+
setIsRebootingCamera(true);
|
|
61
|
+
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
setCameraKey(prev => prev + 1);
|
|
64
|
+
setIsRebootingCamera(false);
|
|
65
|
+
}, 500);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
75
70
|
useEffect(() => {
|
|
76
71
|
if (value && Object.keys(value).length > 0) {
|
|
77
72
|
if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
|
|
78
73
|
const updatedImages = value as Record<string, IIDCardPayload>;
|
|
79
74
|
setCapturedImages(updatedImages);
|
|
80
|
-
|
|
81
75
|
const currentImageData = updatedImages[currentSide];
|
|
82
76
|
if (currentImageData?.dir) {
|
|
83
77
|
setSilentCaptureResult(prev => ({
|
|
84
|
-
...prev,
|
|
85
|
-
path: currentImageData.dir,
|
|
86
|
-
success: true,
|
|
87
|
-
isAnalyzing: false,
|
|
88
|
-
mrz: currentImageData.mrz || '',
|
|
89
|
-
templatePath: currentImageData.templatePath || '',
|
|
78
|
+
...prev, path: currentImageData.dir, success: true, isAnalyzing: false, mrz: currentImageData.mrz || '', templatePath: currentImageData.templatePath || '',
|
|
90
79
|
}));
|
|
91
80
|
}
|
|
92
81
|
}
|
|
@@ -94,9 +83,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
94
83
|
}, [value, currentSide]);
|
|
95
84
|
|
|
96
85
|
const cameraConfig = useMemo(() => {
|
|
97
|
-
const instructions = selectedDocumentType
|
|
98
|
-
? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
|
|
99
|
-
: getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
86
|
+
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
100
87
|
return {
|
|
101
88
|
cameraType: 'back' as const, flashMode: 'auto' as const,
|
|
102
89
|
overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
|
|
@@ -114,36 +101,21 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
114
101
|
};
|
|
115
102
|
|
|
116
103
|
const getCurrentSideVerification = (currentSide: string, countryKey: string) => {
|
|
117
|
-
const rawDocType = countrySelectionData?.documentType;
|
|
118
|
-
|
|
119
|
-
if (!rawDocType || !countryKey) {
|
|
120
|
-
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
121
|
-
}
|
|
122
|
-
|
|
104
|
+
const rawDocType = countrySelectionData?.documentType;
|
|
105
|
+
if (!rawDocType || !countryKey) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
123
106
|
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
|
|
124
107
|
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
125
|
-
|
|
126
108
|
let countryMapping = baseMapping[rawCountryName];
|
|
127
|
-
|
|
128
|
-
// Fallback search in case of case mismatches
|
|
129
|
-
if (!countryMapping) {
|
|
130
|
-
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
131
|
-
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
109
|
if (!countryMapping) {
|
|
135
|
-
|
|
110
|
+
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
111
|
+
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
136
112
|
}
|
|
137
|
-
|
|
113
|
+
if (!countryMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
138
114
|
const regionMapping = countryMapping[rawDocType];
|
|
139
|
-
if (!regionMapping) {
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const authMethod: string[] = [];
|
|
115
|
+
if (!regionMapping) { return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' }; }
|
|
116
|
+
const authMethod: string[] = [];
|
|
144
117
|
const mrzTypes: string[] = [];
|
|
145
118
|
const key = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
|
|
146
|
-
|
|
147
119
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
148
120
|
regionMapping[key].forEach((item: any) => {
|
|
149
121
|
if (item[currentSide]) {
|
|
@@ -152,7 +124,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
152
124
|
}
|
|
153
125
|
});
|
|
154
126
|
}
|
|
155
|
-
|
|
156
127
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
157
128
|
}
|
|
158
129
|
|
|
@@ -219,16 +190,13 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
219
190
|
if (silentCaptureResult.isAnalyzing || isProcessingCapture) return;
|
|
220
191
|
if (result.success && result.path) {
|
|
221
192
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
222
|
-
|
|
223
|
-
// 🚨 Force a template fetch if we haven't successfully saved the current side yet
|
|
224
193
|
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
225
194
|
let templateBbox: IBbox | undefined;
|
|
226
195
|
let templateResponse: any;
|
|
227
|
-
|
|
228
196
|
if (!selectedDocumentType) {
|
|
229
|
-
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
197
|
+
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
198
|
+
return;
|
|
230
199
|
}
|
|
231
|
-
|
|
232
200
|
try {
|
|
233
201
|
if (!templatePath) {
|
|
234
202
|
const templateType = await checkTemplateType({ path: result.path || '', docType: selectedDocumentType?.type as GovernmentDocumentType, docRegion: countryData?.code || "", postfix: currentSide }, env);
|
|
@@ -246,10 +214,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
246
214
|
} catch { }
|
|
247
215
|
}
|
|
248
216
|
}
|
|
249
|
-
|
|
250
217
|
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
251
218
|
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
252
|
-
|
|
253
219
|
let verificationRes: any;
|
|
254
220
|
if (currentSide === 'front') {
|
|
255
221
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
@@ -257,36 +223,18 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
257
223
|
verificationRes = await frontVerification({ path: result.path, regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: countryData?.code || '', currentSide, templatePath, mrzType }, env);
|
|
258
224
|
} else {
|
|
259
225
|
let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
260
|
-
|
|
261
226
|
if (!matchedBackAuthMethod && currentSide === 'back') {
|
|
262
|
-
|
|
227
|
+
matchedBackAuthMethod = 'MRZ';
|
|
263
228
|
}
|
|
264
|
-
|
|
265
229
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
266
|
-
|
|
267
|
-
verificationRes = await backVerification({
|
|
268
|
-
path: result.path,
|
|
269
|
-
regionMapping: {
|
|
270
|
-
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod,
|
|
271
|
-
mrzTypes: regionMappings.mrzTypes
|
|
272
|
-
},
|
|
273
|
-
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
274
|
-
code: countryData?.code || '',
|
|
275
|
-
currentSide,
|
|
276
|
-
templatePath,
|
|
277
|
-
mrzType: backMrzType,
|
|
278
|
-
templateResponse
|
|
279
|
-
}, env);
|
|
230
|
+
verificationRes = await backVerification({ path: result.path, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '', code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse }, env);
|
|
280
231
|
}
|
|
281
|
-
|
|
282
232
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
283
233
|
const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
284
234
|
const verifiedResult: ISilentCaptureResult = { path: result.path, templatePath, bbox, success: true, mrz, isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type };
|
|
285
|
-
|
|
286
235
|
setSilentCaptureResult(verifiedResult);
|
|
287
236
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
288
237
|
await autoCapture(result.path, verifiedResult);
|
|
289
|
-
|
|
290
238
|
} catch (error: any) {
|
|
291
239
|
const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
|
|
292
240
|
const errorMessage = isCardNotFullyInFrame ? t('kyc.idCardCapture.cardNotFullyInFrame') : (error?.message || 'Erreur de détection');
|
|
@@ -296,10 +244,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
296
244
|
};
|
|
297
245
|
|
|
298
246
|
const handleError = (event: { message: string }) => {
|
|
299
|
-
showAlert('Erreur', event.message);
|
|
247
|
+
showAlert('Erreur', event.message);
|
|
248
|
+
setShowCamera(false);
|
|
249
|
+
setIsProcessingCapture(false);
|
|
300
250
|
};
|
|
301
251
|
|
|
302
|
-
useEffect(() => {
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
actions.showCustomStepper(!showCamera);
|
|
254
|
+
}, [showCamera]);
|
|
303
255
|
|
|
304
256
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
305
257
|
return (
|
|
@@ -314,105 +266,133 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
314
266
|
);
|
|
315
267
|
}
|
|
316
268
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
269
|
+
|
|
270
|
+
// --- CAMERA RENDER ---
|
|
271
|
+
if (showCamera) {
|
|
272
|
+
const isBusy = isProcessingCapture;
|
|
273
|
+
|
|
274
|
+
if (isRebootingCamera) {
|
|
320
275
|
return (
|
|
321
|
-
<View style={styles.root}>
|
|
322
|
-
<
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
276
|
+
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
277
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
278
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
279
|
+
</View>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<View style={styles.root}>
|
|
285
|
+
<View style={[styles.cameraWrapper, { flex: 1, minHeight: 400 }]}>
|
|
286
|
+
|
|
287
|
+
<View style={styles.headerContainer}>
|
|
288
|
+
<Text style={styles.headerTitle}>
|
|
289
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
290
|
+
</Text>
|
|
291
|
+
<View style={styles.stepBadge}>
|
|
292
|
+
<Text style={styles.stepText}>
|
|
293
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
328
294
|
</Text>
|
|
329
|
-
<View style={styles.stepBadge}>
|
|
330
|
-
<Text style={styles.stepText}>
|
|
331
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
332
|
-
</Text>
|
|
333
|
-
</View>
|
|
334
295
|
</View>
|
|
296
|
+
</View>
|
|
297
|
+
|
|
298
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
299
|
+
|
|
300
|
+
<EnhancedCameraView
|
|
301
|
+
key={`${currentSide}-${cameraKey}`}
|
|
302
|
+
showCamera={true}
|
|
303
|
+
isProcessing={isBusy}
|
|
304
|
+
cameraType={cameraConfig.cameraType}
|
|
305
|
+
style={styles.absoluteFillObject}
|
|
306
|
+
onError={handleError}
|
|
307
|
+
onSilentCapture={handleSilentCapture}
|
|
308
|
+
silentCaptureResult={silentCaptureResult}
|
|
309
|
+
overlayComponent={
|
|
310
|
+
<>
|
|
311
|
+
{/* We ONLY put the ID frame here, because if the camera fails, we don't care if the frame fails too */}
|
|
312
|
+
<IdCardOverlay
|
|
313
|
+
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
314
|
+
instructions={cameraConfig.overlay.guideText}
|
|
315
|
+
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
316
|
+
isSuccess={silentCaptureResult.success}
|
|
317
|
+
language={state.currentLanguage}
|
|
318
|
+
stepperProps={{
|
|
319
|
+
back: () => {
|
|
320
|
+
if (currentSide === 'back') {
|
|
321
|
+
setCurrentSide('front');
|
|
322
|
+
setShowCamera(false);
|
|
323
|
+
setIsProcessingCapture(false);
|
|
324
|
+
setProcessingImagePath(null);
|
|
325
|
+
if (capturedImages['front']?.dir) {
|
|
326
|
+
const frontImage = capturedImages['front'];
|
|
327
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
328
|
+
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
329
|
+
} else { actions.previousComponent(); }
|
|
330
|
+
},
|
|
331
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
332
|
+
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
333
|
+
}}
|
|
334
|
+
/>
|
|
335
|
+
</>
|
|
336
|
+
}
|
|
337
|
+
/>
|
|
335
338
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
onSilentCapture={handleSilentCapture}
|
|
345
|
-
silentCaptureResult={silentCaptureResult}
|
|
346
|
-
overlayComponent={
|
|
347
|
-
<>
|
|
348
|
-
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
349
|
-
<View style={styles.topAnalyzingPillContainer}>
|
|
350
|
-
<View style={styles.topAnalyzingPill}>
|
|
351
|
-
<ActivityIndicator size="small" color="white" />
|
|
352
|
-
<Text style={styles.analyzingPillText}>
|
|
353
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
354
|
-
</Text>
|
|
355
|
-
</View>
|
|
356
|
-
</View>
|
|
357
|
-
)}
|
|
358
|
-
{isBusy && (
|
|
359
|
-
<View style={StyleSheet.absoluteFillObject}>
|
|
360
|
-
{processingImagePath && (
|
|
361
|
-
<Image
|
|
362
|
-
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
363
|
-
style={StyleSheet.absoluteFillObject}
|
|
364
|
-
resizeMode="cover"
|
|
365
|
-
/>
|
|
366
|
-
)}
|
|
367
|
-
<View style={styles.processingOverlay}>
|
|
368
|
-
<ActivityIndicator size="large" color="#2DBD60" />
|
|
369
|
-
<Text style={styles.processingText}>
|
|
370
|
-
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
371
|
-
</Text>
|
|
372
|
-
</View>
|
|
373
|
-
</View>
|
|
374
|
-
)}
|
|
375
|
-
<IdCardOverlay
|
|
376
|
-
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
377
|
-
instructions={cameraConfig.overlay.guideText}
|
|
378
|
-
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
379
|
-
isSuccess={silentCaptureResult.success}
|
|
380
|
-
language={state.currentLanguage}
|
|
381
|
-
stepperProps={{
|
|
382
|
-
back: () => {
|
|
383
|
-
if (currentSide === 'back') {
|
|
384
|
-
setCurrentSide('front');
|
|
385
|
-
setShowCamera(false);
|
|
386
|
-
setIsProcessingCapture(false);
|
|
387
|
-
setProcessingImagePath(null);
|
|
388
|
-
if (capturedImages['front']?.dir) {
|
|
389
|
-
const frontImage = capturedImages['front'];
|
|
390
|
-
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
391
|
-
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
392
|
-
} else { actions.previousComponent(); }
|
|
393
|
-
},
|
|
394
|
-
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
395
|
-
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
396
|
-
}}
|
|
397
|
-
/>
|
|
398
|
-
|
|
399
|
-
<TouchableOpacity style={styles.refreshButton} onPress={refreshCamera}>
|
|
400
|
-
<Text style={styles.refreshButtonText}>Refresh Camera</Text>
|
|
401
|
-
</TouchableOpacity>
|
|
402
|
-
</>
|
|
403
|
-
}
|
|
404
|
-
/>
|
|
405
|
-
{/* Elegant Floating Error Banner below the cutout */}
|
|
406
|
-
{silentCaptureResult.error && !isBusy ? (
|
|
407
|
-
<View style={styles.floatingErrorBanner}>
|
|
408
|
-
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
339
|
+
|
|
340
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
341
|
+
<View style={styles.topAnalyzingPillContainer}>
|
|
342
|
+
<View style={styles.topAnalyzingPill}>
|
|
343
|
+
<ActivityIndicator size="small" color="white" />
|
|
344
|
+
<Text style={styles.analyzingPillText}>
|
|
345
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
346
|
+
</Text>
|
|
409
347
|
</View>
|
|
410
|
-
|
|
411
|
-
|
|
348
|
+
</View>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{isBusy && (
|
|
352
|
+
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
353
|
+
{processingImagePath && (
|
|
354
|
+
<Image
|
|
355
|
+
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
356
|
+
style={StyleSheet.absoluteFillObject}
|
|
357
|
+
resizeMode="cover"
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
360
|
+
<View style={styles.processingOverlay}>
|
|
361
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
362
|
+
<Text style={styles.processingText}>
|
|
363
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
364
|
+
</Text>
|
|
365
|
+
</View>
|
|
366
|
+
</View>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{!isBusy && (
|
|
370
|
+
<View style={styles.escapeHatchContainer}>
|
|
371
|
+
{/* Refresh Button */}
|
|
372
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
373
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
374
|
+
</TouchableOpacity>
|
|
375
|
+
|
|
376
|
+
<TouchableOpacity
|
|
377
|
+
style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]}
|
|
378
|
+
onPress={() => setShowCamera(false)}
|
|
379
|
+
>
|
|
380
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
381
|
+
</TouchableOpacity>
|
|
382
|
+
</View>
|
|
383
|
+
)}
|
|
384
|
+
|
|
385
|
+
{silentCaptureResult.error && !isBusy ? (
|
|
386
|
+
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
387
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
388
|
+
</View>
|
|
389
|
+
) : null}
|
|
390
|
+
|
|
412
391
|
</View>
|
|
413
392
|
</View>
|
|
414
|
-
|
|
415
|
-
|
|
393
|
+
</View>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
416
396
|
|
|
417
397
|
return (
|
|
418
398
|
<View style={styles.root}>
|
|
@@ -473,132 +453,82 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
473
453
|
};
|
|
474
454
|
|
|
475
455
|
const styles = StyleSheet.create({
|
|
476
|
-
root: {
|
|
477
|
-
flex: 1,
|
|
478
|
-
|
|
479
|
-
backgroundColor: 'transparent',
|
|
480
|
-
alignSelf: 'center',
|
|
481
|
-
...(Platform.OS === 'web'
|
|
482
|
-
? ({
|
|
483
|
-
minHeight: '85vh',
|
|
484
|
-
justifyContent: 'center',
|
|
485
|
-
alignItems: 'center',
|
|
486
|
-
// Note: backdropFilter is valid in React Native Web but TS might complain, cast safely
|
|
487
|
-
backdropFilter: 'blur(8px)'
|
|
488
|
-
} as any)
|
|
489
|
-
: {})
|
|
456
|
+
root: {
|
|
457
|
+
flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
|
|
458
|
+
...(Platform.OS === 'web' ? ({ minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } as any) : {})
|
|
490
459
|
},
|
|
491
460
|
cameraWrapper: {
|
|
492
|
-
width: '100%',
|
|
493
|
-
|
|
494
|
-
overflow: 'hidden',
|
|
495
|
-
...(Platform.OS === 'web'
|
|
496
|
-
? ({
|
|
497
|
-
maxWidth: 500,
|
|
498
|
-
height: 700,
|
|
499
|
-
maxHeight: '90vh', // TypeScript will now ignore this thanks to the cast below
|
|
500
|
-
borderRadius: 24,
|
|
501
|
-
shadowColor: '#000',
|
|
502
|
-
shadowOffset: { width: 0, height: 20 },
|
|
503
|
-
shadowOpacity: 0.25,
|
|
504
|
-
shadowRadius: 35,
|
|
505
|
-
elevation: 24,
|
|
506
|
-
} as any) // 🚨 CAST TO ANY
|
|
507
|
-
: {
|
|
508
|
-
flex: 1,
|
|
509
|
-
})
|
|
461
|
+
width: '100%', backgroundColor: '#FFFFFF', overflow: 'hidden',
|
|
462
|
+
...(Platform.OS === 'web' ? ({ maxWidth: 500, height: 700, maxHeight: '90vh', borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24, } as any) : { flex: 1, })
|
|
510
463
|
},
|
|
511
464
|
headerContainer: {
|
|
512
|
-
flexDirection: 'row',
|
|
513
|
-
|
|
514
|
-
justifyContent: 'space-between',
|
|
515
|
-
paddingHorizontal: 24,
|
|
516
|
-
paddingVertical: 18,
|
|
517
|
-
backgroundColor: '#FFFFFF',
|
|
518
|
-
borderBottomWidth: 1,
|
|
519
|
-
borderBottomColor: '#F1F5F9',
|
|
520
|
-
zIndex: 10,
|
|
521
|
-
// Mobile hidden, Web visible to replace floating text
|
|
522
|
-
...(Platform.OS !== 'web' ? { display: 'none' } : {})
|
|
523
|
-
},
|
|
524
|
-
headerTitle: {
|
|
525
|
-
fontSize: 18,
|
|
526
|
-
fontWeight: '700',
|
|
527
|
-
color: '#0F172A',
|
|
465
|
+
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
|
|
466
|
+
...(Platform.OS !== 'web' ? { display: 'none' } : {})
|
|
528
467
|
},
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
468
|
+
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
|
|
469
|
+
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
470
|
+
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
471
|
+
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
472
|
+
camera: { flex: 1, },
|
|
473
|
+
refreshButton: {
|
|
474
|
+
position: 'absolute', bottom: 100, alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 24, zIndex: 500
|
|
534
475
|
},
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
476
|
+
refreshButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
477
|
+
previewContainer: {
|
|
478
|
+
width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
|
|
479
|
+
...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
|
|
539
480
|
},
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
481
|
+
previewItemContainer: { flexGrow: 1, },
|
|
482
|
+
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
483
|
+
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
484
|
+
sideContainer: { marginBottom: 24 },
|
|
485
|
+
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
486
|
+
imagePreviewWrapper: { width: '100%', height: 220, borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0' },
|
|
487
|
+
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
488
|
+
floatingErrorBanner: {
|
|
489
|
+
position: 'absolute', bottom: 30, left: 24, right: 24, backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', shadowColor: '#DC2626', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 8, zIndex: 100
|
|
544
490
|
},
|
|
545
|
-
|
|
546
|
-
|
|
491
|
+
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
492
|
+
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
493
|
+
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
494
|
+
warningBanner: { backgroundColor: '#FF9500', padding: 12, borderRadius: 8, width: '100%', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 4 },
|
|
495
|
+
warningText: { color: 'white', fontWeight: 'bold', textAlign: 'center', fontSize: 16 },
|
|
496
|
+
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
497
|
+
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
498
|
+
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
499
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
500
|
+
escapeHatchContainer: {
|
|
501
|
+
position: 'absolute',
|
|
502
|
+
bottom: 40,
|
|
503
|
+
left: 0,
|
|
504
|
+
right: 0,
|
|
505
|
+
alignItems: 'center',
|
|
506
|
+
justifyContent: 'center',
|
|
507
|
+
zIndex: 99999, // Guarantees it is the top-most element
|
|
547
508
|
},
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
backgroundColor: 'white',
|
|
551
|
-
borderRadius: 12,
|
|
552
|
-
paddingVertical: 24,
|
|
553
|
-
paddingHorizontal: 20,
|
|
554
|
-
shadowColor: '#000',
|
|
555
|
-
shadowOffset: { width: 0, height: 4 },
|
|
556
|
-
shadowOpacity: 0.1,
|
|
557
|
-
shadowRadius: 12,
|
|
558
|
-
elevation: 8,
|
|
559
|
-
...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
|
|
560
|
-
},
|
|
561
|
-
previewItemContainer: {
|
|
562
|
-
flexGrow: 1,
|
|
563
|
-
},
|
|
564
|
-
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
565
|
-
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
566
|
-
sideContainer: { marginBottom: 24 },
|
|
567
|
-
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
568
|
-
imagePreviewWrapper: {
|
|
569
|
-
width: '100%', height: 220, borderRadius: 12, padding: 1, overflow: 'hidden',
|
|
570
|
-
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0'
|
|
571
|
-
},
|
|
572
|
-
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
573
|
-
floatingErrorBanner: {
|
|
574
|
-
position: 'absolute',
|
|
575
|
-
bottom: 30, // Pushed to the bottom for professional feel
|
|
576
|
-
left: 24,
|
|
577
|
-
right: 24,
|
|
578
|
-
backgroundColor: '#FEF2F2',
|
|
509
|
+
fallbackRefreshButton: {
|
|
510
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)', // Darker so it's visible on white or black
|
|
579
511
|
borderWidth: 1,
|
|
580
|
-
borderColor: '
|
|
581
|
-
paddingVertical: 12,
|
|
582
|
-
paddingHorizontal:
|
|
583
|
-
borderRadius:
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
refreshButton: { position: 'absolute', bottom: 100, alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.5)', padding: 10, borderRadius: 20, zIndex: 500 },
|
|
603
|
-
refreshButtonText: { color: 'white', fontWeight: 'bold' },
|
|
512
|
+
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
513
|
+
paddingVertical: 12,
|
|
514
|
+
paddingHorizontal: 24,
|
|
515
|
+
borderRadius: 24,
|
|
516
|
+
shadowColor: '#000',
|
|
517
|
+
shadowOffset: { width: 0, height: 4 },
|
|
518
|
+
shadowOpacity: 0.3,
|
|
519
|
+
shadowRadius: 5,
|
|
520
|
+
elevation: 8,
|
|
521
|
+
},
|
|
522
|
+
fallbackRefreshText: {
|
|
523
|
+
color: '#FFFFFF',
|
|
524
|
+
fontWeight: 'bold',
|
|
525
|
+
fontSize: 16,
|
|
526
|
+
},
|
|
527
|
+
absoluteFillObject: {
|
|
528
|
+
position: 'absolute',
|
|
529
|
+
top: 0,
|
|
530
|
+
left: 0,
|
|
531
|
+
right: 0,
|
|
532
|
+
bottom: 0,
|
|
533
|
+
},
|
|
604
534
|
});
|