@sanctum-key/react-native-sdk 1.0.9 → 1.0.10
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/CountrySelection.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelection.js +259 -63
- package/build/src/components/KYCElements/CountrySelection.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +222 -69
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js +160 -21
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
- package/build/src/config/region_mapping.json +727 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +3 -7
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +1 -2
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +106 -59
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/CountrySelection.tsx +300 -74
- package/src/components/KYCElements/IDCardCapture.tsx +310 -156
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +201 -29
- package/src/modules/api/CardAuthentification.ts +5 -8
- package/src/modules/api/KYCService.ts +167 -105
|
@@ -12,6 +12,14 @@ import { backVerification, checkTemplateType, frontVerification } from '../../mo
|
|
|
12
12
|
import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
|
|
13
13
|
import pathToBase64 from '../../utils/pathToBase64';
|
|
14
14
|
import { cropByObb, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
|
|
15
|
+
import REGION_MAPPING from '../../config/region_mapping.json';
|
|
16
|
+
|
|
17
|
+
// 🌍 Map ISO codes to exact JSON root keys to ensure perfect dictionary lookups
|
|
18
|
+
const ISO_TO_COUNTRY_NAME: Record<string, string> = {
|
|
19
|
+
'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
|
|
20
|
+
'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
|
|
21
|
+
'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
|
|
22
|
+
};
|
|
15
23
|
|
|
16
24
|
interface IIDCardPayload { dir: string; file: string; mrz: string; templatePath?: string; }
|
|
17
25
|
interface IDCardCaptureProps {
|
|
@@ -30,7 +38,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
30
38
|
const [currentSide, setCurrentSide] = useState<'front' | 'back'>('front');
|
|
31
39
|
const [bboxBySide, setBboxBySide] = useState<Record<string, IBbox>>({});
|
|
32
40
|
const [silentCaptureResult, setSilentCaptureResult] = useState<ISilentCaptureResult>({ success: false, isAnalyzing: false });
|
|
33
|
-
|
|
34
41
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
35
42
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
36
43
|
|
|
@@ -65,23 +72,26 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
65
72
|
if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
|
|
66
73
|
const updatedImages = value as Record<string, IIDCardPayload>;
|
|
67
74
|
setCapturedImages(updatedImages);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
const currentImageData = updatedImages[currentSide];
|
|
77
|
+
if (currentImageData?.dir) {
|
|
78
|
+
setSilentCaptureResult(prev => ({
|
|
79
|
+
...prev,
|
|
80
|
+
path: currentImageData.dir,
|
|
81
|
+
success: true,
|
|
82
|
+
isAnalyzing: false,
|
|
83
|
+
mrz: currentImageData.mrz || '',
|
|
84
|
+
templatePath: currentImageData.templatePath || '',
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
|
-
}, [value]);
|
|
89
|
+
}, [value, currentSide]);
|
|
79
90
|
|
|
80
91
|
const cameraConfig = useMemo(() => {
|
|
81
92
|
const instructions = selectedDocumentType
|
|
82
93
|
? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
|
|
83
94
|
: getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
84
|
-
|
|
85
95
|
return {
|
|
86
96
|
cameraType: 'back' as const, flashMode: 'auto' as const,
|
|
87
97
|
overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
|
|
@@ -89,31 +99,55 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
89
99
|
}, [selectedDocumentType, locale, component.instructions]);
|
|
90
100
|
|
|
91
101
|
const retakePicture = (sideToRetake: 'front' | 'back') => {
|
|
92
|
-
// Completely wipe all processing states to prevent leakage
|
|
93
102
|
setIsProcessingCapture(false);
|
|
94
103
|
setProcessingImagePath(null);
|
|
95
104
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
96
|
-
|
|
97
|
-
setShowCamera(true);
|
|
105
|
+
setShowCamera(true);
|
|
98
106
|
actions.showCustomStepper(false);
|
|
99
|
-
|
|
100
107
|
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
101
108
|
if (value) { const newValue = { ...value }; delete newValue[sideToRetake]; onValueChange(newValue); }
|
|
102
109
|
};
|
|
103
110
|
|
|
104
|
-
const getCurrentSideVerification = (currentSide: string) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const getCurrentSideVerification = (currentSide: string, countryKey: string) => {
|
|
112
|
+
const rawDocType = countrySelectionData?.documentType;
|
|
113
|
+
|
|
114
|
+
if (!rawDocType || !countryKey) {
|
|
115
|
+
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
|
|
119
|
+
const baseMapping = (REGION_MAPPING as any).regionMapping || REGION_MAPPING;
|
|
120
|
+
|
|
121
|
+
let countryMapping = baseMapping[rawCountryName];
|
|
122
|
+
|
|
123
|
+
// Fallback search in case of case mismatches
|
|
124
|
+
if (!countryMapping) {
|
|
125
|
+
const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
|
|
126
|
+
if (foundKey) countryMapping = baseMapping[foundKey];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!countryMapping) {
|
|
130
|
+
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const regionMapping = countryMapping[rawDocType];
|
|
134
|
+
if (!regionMapping) {
|
|
135
|
+
return { authMethod: [], mrzTypes: [], regionMapping: null, key: 'root' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const authMethod: string[] = [];
|
|
139
|
+
const mrzTypes: string[] = [];
|
|
140
|
+
const key = countrySelectionData.region?.trim()?.length > 0 ? countrySelectionData.region.trim() : 'root';
|
|
141
|
+
|
|
109
142
|
if (regionMapping?.[key] && Array.isArray(regionMapping[key])) {
|
|
110
143
|
regionMapping[key].forEach((item: any) => {
|
|
111
|
-
if (item[currentSide
|
|
112
|
-
authMethod.push(item[currentSide
|
|
144
|
+
if (item[currentSide]) {
|
|
145
|
+
authMethod.push(item[currentSide]);
|
|
113
146
|
if (item?.mrz_type) mrzTypes.push(item?.mrz_type);
|
|
114
147
|
}
|
|
115
148
|
});
|
|
116
149
|
}
|
|
150
|
+
|
|
117
151
|
return { authMethod: removeDuplicates(authMethod), mrzTypes: removeDuplicates(mrzTypes), regionMapping, key };
|
|
118
152
|
}
|
|
119
153
|
|
|
@@ -121,7 +155,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
121
155
|
if (!mapping || !mapping[selectedDocumentType]) return null;
|
|
122
156
|
const fileName = templatePath.split("/").pop()?.replace(".jpg", "").replace(".png", "");
|
|
123
157
|
if (!fileName) return null;
|
|
124
|
-
const pyName = `${fileName}.py`;
|
|
158
|
+
const pyName = `${fileName}.py`;
|
|
125
159
|
const found = mapping[selectedDocumentType].find((item: any) => item.py_file === pyName);
|
|
126
160
|
return found?.mrz_type || null;
|
|
127
161
|
}
|
|
@@ -137,10 +171,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
137
171
|
|
|
138
172
|
const autoCapture = async (capturePath: string, verified: ISilentCaptureResult) => {
|
|
139
173
|
if (isProcessingCapture) return;
|
|
140
|
-
|
|
141
|
-
setIsProcessingCapture(true);
|
|
174
|
+
setIsProcessingCapture(true);
|
|
142
175
|
setProcessingImagePath(capturePath);
|
|
143
|
-
|
|
144
176
|
try {
|
|
145
177
|
let imagePathForUpload = capturePath;
|
|
146
178
|
if (verified.bbox) {
|
|
@@ -150,13 +182,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
150
182
|
imagePathForUpload = capturePath;
|
|
151
183
|
}
|
|
152
184
|
}
|
|
153
|
-
|
|
154
185
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
155
186
|
const newImages = {
|
|
156
187
|
...capturedImages,
|
|
157
188
|
[currentSide]: { dir: imagePathForUpload, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
|
|
158
189
|
};
|
|
159
|
-
|
|
160
190
|
setCapturedImages(newImages);
|
|
161
191
|
if (verified.country && verified.documentType) {
|
|
162
192
|
onValueChange({
|
|
@@ -165,15 +195,13 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
165
195
|
documentType: GovernmentDocumentTypeBackend[verified.documentType as keyof typeof GovernmentDocumentTypeBackend] || '',
|
|
166
196
|
});
|
|
167
197
|
}
|
|
168
|
-
|
|
169
198
|
setTimeout(() => {
|
|
170
199
|
setShowCamera(false);
|
|
171
200
|
actions.showCustomStepper(true);
|
|
172
201
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
|
|
173
202
|
setIsProcessingCapture(false);
|
|
174
|
-
setProcessingImagePath(null);
|
|
203
|
+
setProcessingImagePath(null);
|
|
175
204
|
}, 600);
|
|
176
|
-
|
|
177
205
|
} catch (e: any) {
|
|
178
206
|
showAlert('Error', e?.message || 'Impossible de capturer la photo');
|
|
179
207
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
@@ -184,25 +212,23 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
184
212
|
|
|
185
213
|
const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string }) => {
|
|
186
214
|
if (silentCaptureResult.isAnalyzing || isProcessingCapture) return;
|
|
187
|
-
|
|
188
215
|
if (result.success && result.path) {
|
|
189
216
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
|
|
190
|
-
|
|
217
|
+
|
|
218
|
+
// 🚨 Force a template fetch if we haven't successfully saved the current side yet
|
|
219
|
+
let templatePath = capturedImages[currentSide]?.templatePath || '';
|
|
191
220
|
let templateBbox: IBbox | undefined;
|
|
192
221
|
let templateResponse: any;
|
|
193
|
-
|
|
222
|
+
|
|
194
223
|
if (!selectedDocumentType) {
|
|
195
224
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' })); return;
|
|
196
225
|
}
|
|
197
226
|
|
|
198
|
-
const regionMappings = getCurrentSideVerification(currentSide);
|
|
199
|
-
|
|
200
227
|
try {
|
|
201
|
-
if (templatePath
|
|
228
|
+
if (!templatePath) {
|
|
202
229
|
const templateType = await checkTemplateType({ path: result.path || '', docType: selectedDocumentType?.type as GovernmentDocumentType, docRegion: countryData?.code || "", postfix: currentSide }, env);
|
|
203
230
|
templateResponse = templateType;
|
|
204
231
|
if (templateType.template_path) templatePath = templateType.template_path;
|
|
205
|
-
|
|
206
232
|
if (templateType.card_obb) {
|
|
207
233
|
const obbConfidence = getObbConfidence((templateType as any).card_obb);
|
|
208
234
|
if (obbConfidence !== null && obbConfidence < OBB_CONFIDENCE_THRESHOLD) {
|
|
@@ -216,26 +242,46 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
216
242
|
}
|
|
217
243
|
}
|
|
218
244
|
|
|
245
|
+
const extractedCountryKey = templatePath ? templatePath.split('/')[0] : (ISO_TO_COUNTRY_NAME[countryData?.code || ''] || 'root');
|
|
246
|
+
const regionMappings = getCurrentSideVerification(currentSide, extractedCountryKey);
|
|
247
|
+
|
|
219
248
|
let verificationRes: any;
|
|
220
249
|
if (currentSide === 'front') {
|
|
221
250
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
222
|
-
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
251
|
+
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
223
252
|
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);
|
|
224
253
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
255
|
+
|
|
256
|
+
if (!matchedBackAuthMethod && currentSide === 'back') {
|
|
257
|
+
matchedBackAuthMethod = 'MRZ';
|
|
258
|
+
}
|
|
229
259
|
|
|
260
|
+
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
261
|
+
|
|
262
|
+
verificationRes = await backVerification({
|
|
263
|
+
path: result.path,
|
|
264
|
+
regionMapping: {
|
|
265
|
+
authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod,
|
|
266
|
+
mrzTypes: regionMappings.mrzTypes
|
|
267
|
+
},
|
|
268
|
+
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type as keyof typeof GovernmentDocumentTypeShorted] || '',
|
|
269
|
+
code: countryData?.code || '',
|
|
270
|
+
currentSide,
|
|
271
|
+
templatePath,
|
|
272
|
+
mrzType: backMrzType,
|
|
273
|
+
templateResponse
|
|
274
|
+
}, env);
|
|
275
|
+
}
|
|
276
|
+
|
|
230
277
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
231
278
|
const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
232
|
-
|
|
233
279
|
const verifiedResult: ISilentCaptureResult = { path: result.path, templatePath, bbox, success: true, mrz, isAnalyzing: false, country: countryData?.code, documentType: selectedDocumentType.type };
|
|
280
|
+
|
|
234
281
|
setSilentCaptureResult(verifiedResult);
|
|
235
282
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
236
|
-
|
|
237
283
|
await autoCapture(result.path, verifiedResult);
|
|
238
|
-
|
|
284
|
+
|
|
239
285
|
} catch (error: any) {
|
|
240
286
|
const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
|
|
241
287
|
const errorMessage = isCardNotFullyInFrame ? t('kyc.idCardCapture.cardNotFullyInFrame') : (error?.message || 'Erreur de détection');
|
|
@@ -253,7 +299,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
253
299
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
254
300
|
return (
|
|
255
301
|
<View style={styles.root}>
|
|
256
|
-
<View style={styles.
|
|
302
|
+
<View style={styles.previewContainer}>
|
|
257
303
|
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
258
304
|
<Text style={styles.description}>
|
|
259
305
|
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
@@ -265,84 +311,96 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
265
311
|
|
|
266
312
|
// --- CAMERA RENDER ---
|
|
267
313
|
if (showCamera) {
|
|
268
|
-
const isBusy = isProcessingCapture;
|
|
269
|
-
|
|
314
|
+
const isBusy = isProcessingCapture;
|
|
270
315
|
return (
|
|
271
|
-
<View style={styles.
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<View style={styles.topAnalyzingPill}>
|
|
286
|
-
<ActivityIndicator size="small" color="white" />
|
|
287
|
-
<Text style={styles.analyzingPillText}>
|
|
288
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
289
|
-
</Text>
|
|
290
|
-
</View>
|
|
291
|
-
</View>
|
|
292
|
-
)}
|
|
316
|
+
<View style={styles.root}>
|
|
317
|
+
<View style={styles.cameraWrapper}>
|
|
318
|
+
|
|
319
|
+
{/* Web/Desktop Clean Header */}
|
|
320
|
+
<View style={styles.headerContainer}>
|
|
321
|
+
<Text style={styles.headerTitle}>
|
|
322
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
323
|
+
</Text>
|
|
324
|
+
<View style={styles.stepBadge}>
|
|
325
|
+
<Text style={styles.stepText}>
|
|
326
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
327
|
+
</Text>
|
|
328
|
+
</View>
|
|
329
|
+
</View>
|
|
293
330
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
331
|
+
<View style={styles.cameraFeedContainer}>
|
|
332
|
+
<EnhancedCameraView
|
|
333
|
+
key={currentSide}
|
|
334
|
+
showCamera={true}
|
|
335
|
+
isProcessing={isBusy}
|
|
336
|
+
cameraType={cameraConfig.cameraType}
|
|
337
|
+
style={styles.camera}
|
|
338
|
+
onError={handleError}
|
|
339
|
+
onSilentCapture={handleSilentCapture}
|
|
340
|
+
silentCaptureResult={silentCaptureResult}
|
|
341
|
+
overlayComponent={
|
|
342
|
+
<>
|
|
343
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
344
|
+
<View style={styles.topAnalyzingPillContainer}>
|
|
345
|
+
<View style={styles.topAnalyzingPill}>
|
|
346
|
+
<ActivityIndicator size="small" color="white" />
|
|
347
|
+
<Text style={styles.analyzingPillText}>
|
|
348
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
349
|
+
</Text>
|
|
350
|
+
</View>
|
|
351
|
+
</View>
|
|
302
352
|
)}
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
353
|
+
{isBusy && (
|
|
354
|
+
<View style={StyleSheet.absoluteFillObject}>
|
|
355
|
+
{processingImagePath && (
|
|
356
|
+
<Image
|
|
357
|
+
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
358
|
+
style={StyleSheet.absoluteFillObject}
|
|
359
|
+
resizeMode="cover"
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
<View style={styles.processingOverlay}>
|
|
363
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
364
|
+
<Text style={styles.processingText}>
|
|
365
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
366
|
+
</Text>
|
|
367
|
+
</View>
|
|
368
|
+
</View>
|
|
369
|
+
)}
|
|
370
|
+
<IdCardOverlay
|
|
371
|
+
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
372
|
+
instructions={cameraConfig.overlay.guideText}
|
|
373
|
+
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
374
|
+
isSuccess={silentCaptureResult.success}
|
|
375
|
+
language={state.currentLanguage}
|
|
376
|
+
stepperProps={{
|
|
377
|
+
back: () => {
|
|
378
|
+
if (currentSide === 'back') {
|
|
379
|
+
setCurrentSide('front');
|
|
380
|
+
setShowCamera(false);
|
|
381
|
+
setIsProcessingCapture(false);
|
|
382
|
+
setProcessingImagePath(null);
|
|
383
|
+
if (capturedImages['front']?.dir) {
|
|
384
|
+
const frontImage = capturedImages['front'];
|
|
385
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
386
|
+
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
387
|
+
} else { actions.previousComponent(); }
|
|
388
|
+
},
|
|
389
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
390
|
+
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
</>
|
|
394
|
+
}
|
|
395
|
+
/>
|
|
396
|
+
{/* Elegant Floating Error Banner below the cutout */}
|
|
397
|
+
{silentCaptureResult.error && !isBusy ? (
|
|
398
|
+
<View style={styles.floatingErrorBanner}>
|
|
399
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
400
|
+
</View>
|
|
401
|
+
) : null}
|
|
344
402
|
</View>
|
|
345
|
-
|
|
403
|
+
</View>
|
|
346
404
|
</View>
|
|
347
405
|
);
|
|
348
406
|
}
|
|
@@ -359,7 +417,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
359
417
|
{getLocalizedText(component.instructions)}
|
|
360
418
|
</Text>
|
|
361
419
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
362
|
-
|
|
363
420
|
{silentCaptureResult?.error === 'TOO_FAR_AWAY' && (
|
|
364
421
|
<View style={styles.warningBanner}>
|
|
365
422
|
<Text style={styles.warningText}>
|
|
@@ -367,7 +424,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
367
424
|
</Text>
|
|
368
425
|
</View>
|
|
369
426
|
)}
|
|
370
|
-
|
|
371
427
|
<View style={styles.imagePreviewWrapper}>
|
|
372
428
|
{capturedImages[currentSide]?.dir ? (
|
|
373
429
|
<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage} />
|
|
@@ -375,7 +431,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
375
431
|
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
376
432
|
) : null}
|
|
377
433
|
</View>
|
|
378
|
-
|
|
379
434
|
{!capturedImages[currentSide]?.dir && (
|
|
380
435
|
<Button
|
|
381
436
|
title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
|
|
@@ -383,22 +438,19 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
383
438
|
variant="primary" size="large" fullWidth
|
|
384
439
|
/>
|
|
385
440
|
)}
|
|
386
|
-
|
|
387
441
|
{capturedImages[currentSide]?.dir && (
|
|
388
442
|
<>
|
|
389
443
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth />
|
|
390
444
|
<Button title={t('common.next')} onPress={() => {
|
|
391
445
|
if (!selectedDocumentType) { showAlert('Error', 'Document type not selected'); return; }
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
setIsProcessingCapture(false);
|
|
401
|
-
setProcessingImagePath(null);
|
|
446
|
+
if (currentSide === 'back' || selectedDocumentType.type === 'passport') {
|
|
447
|
+
actions.nextComponent();
|
|
448
|
+
} else {
|
|
449
|
+
setShowCamera(true);
|
|
450
|
+
setCurrentSide('back');
|
|
451
|
+
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
452
|
+
setIsProcessingCapture(false);
|
|
453
|
+
setProcessingImagePath(null);
|
|
402
454
|
}
|
|
403
455
|
}} variant="primary" size="large" fullWidth />
|
|
404
456
|
</>
|
|
@@ -412,28 +464,130 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
412
464
|
};
|
|
413
465
|
|
|
414
466
|
const styles = StyleSheet.create({
|
|
415
|
-
root: {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
467
|
+
root: {
|
|
468
|
+
flex: 1,
|
|
469
|
+
width: '100%',
|
|
470
|
+
backgroundColor: 'transparent',
|
|
471
|
+
alignSelf: 'center',
|
|
472
|
+
...(Platform.OS === 'web'
|
|
473
|
+
? ({
|
|
474
|
+
minHeight: '85vh',
|
|
475
|
+
justifyContent: 'center',
|
|
476
|
+
alignItems: 'center',
|
|
477
|
+
// Note: backdropFilter is valid in React Native Web but TS might complain, cast safely
|
|
478
|
+
backdropFilter: 'blur(8px)'
|
|
479
|
+
} as any)
|
|
480
|
+
: {})
|
|
481
|
+
},
|
|
482
|
+
cameraWrapper: {
|
|
483
|
+
width: '100%',
|
|
484
|
+
backgroundColor: '#FFFFFF',
|
|
485
|
+
overflow: 'hidden',
|
|
486
|
+
...(Platform.OS === 'web'
|
|
487
|
+
? ({
|
|
488
|
+
maxWidth: 500,
|
|
489
|
+
height: 700,
|
|
490
|
+
maxHeight: '90vh', // TypeScript will now ignore this thanks to the cast below
|
|
491
|
+
borderRadius: 24,
|
|
492
|
+
shadowColor: '#000',
|
|
493
|
+
shadowOffset: { width: 0, height: 20 },
|
|
494
|
+
shadowOpacity: 0.25,
|
|
495
|
+
shadowRadius: 35,
|
|
496
|
+
elevation: 24,
|
|
497
|
+
} as any) // 🚨 CAST TO ANY
|
|
498
|
+
: {
|
|
499
|
+
flex: 1,
|
|
500
|
+
})
|
|
501
|
+
},
|
|
502
|
+
headerContainer: {
|
|
503
|
+
flexDirection: 'row',
|
|
504
|
+
alignItems: 'center',
|
|
505
|
+
justifyContent: 'space-between',
|
|
506
|
+
paddingHorizontal: 24,
|
|
507
|
+
paddingVertical: 18,
|
|
508
|
+
backgroundColor: '#FFFFFF',
|
|
509
|
+
borderBottomWidth: 1,
|
|
510
|
+
borderBottomColor: '#F1F5F9',
|
|
511
|
+
zIndex: 10,
|
|
512
|
+
// Mobile hidden, Web visible to replace floating text
|
|
513
|
+
...(Platform.OS !== 'web' ? { display: 'none' } : {})
|
|
514
|
+
},
|
|
515
|
+
headerTitle: {
|
|
516
|
+
fontSize: 18,
|
|
517
|
+
fontWeight: '700',
|
|
518
|
+
color: '#0F172A',
|
|
519
|
+
},
|
|
520
|
+
stepBadge: {
|
|
521
|
+
backgroundColor: '#F1F5F9',
|
|
522
|
+
paddingHorizontal: 12,
|
|
523
|
+
paddingVertical: 6,
|
|
524
|
+
borderRadius: 20,
|
|
525
|
+
},
|
|
526
|
+
stepText: {
|
|
527
|
+
fontSize: 13,
|
|
528
|
+
fontWeight: '600',
|
|
529
|
+
color: '#64748B',
|
|
530
|
+
},
|
|
531
|
+
cameraFeedContainer: {
|
|
532
|
+
flex: 1,
|
|
533
|
+
position: 'relative',
|
|
534
|
+
backgroundColor: '#000',
|
|
535
|
+
},
|
|
436
536
|
camera: {
|
|
437
537
|
flex: 1,
|
|
438
538
|
},
|
|
539
|
+
previewContainer: {
|
|
540
|
+
width: '100%',
|
|
541
|
+
backgroundColor: 'white',
|
|
542
|
+
borderRadius: 12,
|
|
543
|
+
paddingVertical: 24,
|
|
544
|
+
paddingHorizontal: 20,
|
|
545
|
+
shadowColor: '#000',
|
|
546
|
+
shadowOffset: { width: 0, height: 4 },
|
|
547
|
+
shadowOpacity: 0.1,
|
|
548
|
+
shadowRadius: 12,
|
|
549
|
+
elevation: 8,
|
|
550
|
+
...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
|
|
551
|
+
},
|
|
552
|
+
previewItemContainer: {
|
|
553
|
+
flexGrow: 1,
|
|
554
|
+
},
|
|
555
|
+
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
556
|
+
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
557
|
+
sideContainer: { marginBottom: 24 },
|
|
558
|
+
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
559
|
+
imagePreviewWrapper: {
|
|
560
|
+
width: '100%', height: 220, borderRadius: 12, padding: 1, overflow: 'hidden',
|
|
561
|
+
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0'
|
|
562
|
+
},
|
|
563
|
+
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
564
|
+
floatingErrorBanner: {
|
|
565
|
+
position: 'absolute',
|
|
566
|
+
bottom: 30, // Pushed to the bottom for professional feel
|
|
567
|
+
left: 24,
|
|
568
|
+
right: 24,
|
|
569
|
+
backgroundColor: '#FEF2F2',
|
|
570
|
+
borderWidth: 1,
|
|
571
|
+
borderColor: '#FCA5A5',
|
|
572
|
+
paddingVertical: 12,
|
|
573
|
+
paddingHorizontal: 16,
|
|
574
|
+
borderRadius: 12,
|
|
575
|
+
alignItems: 'center',
|
|
576
|
+
justifyContent: 'center',
|
|
577
|
+
shadowColor: '#DC2626',
|
|
578
|
+
shadowOffset: { width: 0, height: 4 },
|
|
579
|
+
shadowOpacity: 0.1,
|
|
580
|
+
shadowRadius: 8,
|
|
581
|
+
elevation: 8,
|
|
582
|
+
zIndex: 100
|
|
583
|
+
},
|
|
584
|
+
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
585
|
+
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
586
|
+
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
587
|
+
warningBanner: { backgroundColor: '#FF9500', padding: 12, borderRadius: 8, width: '100%', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 4 },
|
|
588
|
+
warningText: { color: 'white', fontWeight: 'bold', textAlign: 'center', fontSize: 16 },
|
|
589
|
+
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
590
|
+
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
591
|
+
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
592
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
439
593
|
});
|