@sanctum-key/react-native-sdk 1.0.18 → 1.0.20
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/README.md +1 -1
- package/build/package.json +3 -2
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +19 -182
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
- package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +224 -189
- 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 +0 -2
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/components/OverLay/IdCard.d.ts +6 -1
- package/build/src/components/OverLay/IdCard.d.ts.map +1 -1
- package/build/src/components/OverLay/IdCard.js +36 -34
- package/build/src/components/OverLay/IdCard.js.map +1 -1
- package/build/src/config/countriesData.d.ts.map +1 -1
- package/build/src/config/countriesData.js.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts +5 -0
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +67 -14
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +10 -0
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +135 -49
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/web/WebKYCEntry.d.ts.map +1 -1
- package/build/src/web/WebKYCEntry.js +34 -6
- package/build/src/web/WebKYCEntry.js.map +1 -1
- package/package.json +3 -2
- package/src/components/EnhancedCameraView.tsx +28 -219
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
- package/src/components/KYCElements/IDCardCapture.tsx +606 -570
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
- package/src/components/OverLay/IdCard.tsx +48 -36
- package/src/config/countriesData.ts +0 -4
- package/src/hooks/useTemplateKYCFlow.tsx +14 -14
- package/src/modules/api/CardAuthentification.ts +133 -67
- package/src/modules/api/KYCService.ts +189 -66
- package/src/web/WebKYCEntry.tsx +42 -7
|
@@ -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, useWindowDimensions } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -13,6 +13,7 @@ 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
15
|
import REGION_MAPPING from '../../config/region_mapping.json';
|
|
16
|
+
import QRCode from 'react-native-qrcode-svg';
|
|
16
17
|
const ISO_TO_COUNTRY_NAME = { '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' };
|
|
17
18
|
export const IDCardCapture = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
|
|
18
19
|
const { t, locale } = useI18n();
|
|
@@ -25,10 +26,17 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
25
26
|
const [processingImagePath, setProcessingImagePath] = useState(null);
|
|
26
27
|
const [cameraKey, setCameraKey] = useState(0);
|
|
27
28
|
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
28
|
-
// Web specific state
|
|
29
29
|
const [cameraType, setCameraType] = useState('back');
|
|
30
|
+
const [showQRModal, setShowQRModal] = useState(false);
|
|
31
|
+
const [currentUrl, setCurrentUrl] = useState('');
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
34
|
+
setCurrentUrl(window.location.href);
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
30
37
|
const documentTypeMapping = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
|
|
31
38
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
39
|
+
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
32
40
|
const getLocalizedText = (text) => {
|
|
33
41
|
if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
|
|
34
42
|
return text[currentSide][locale] || '';
|
|
@@ -73,15 +81,33 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
73
81
|
}, [value, currentSide]);
|
|
74
82
|
const cameraConfig = useMemo(() => {
|
|
75
83
|
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
|
|
84
|
+
const isLandscape = screenWidth > screenHeight;
|
|
85
|
+
let xMin, xMax, yMin, yMax;
|
|
86
|
+
if (Platform.OS === 'web') {
|
|
87
|
+
const boxWidthPercent = isLandscape ? 0.40 : 0.85;
|
|
88
|
+
xMin = isLandscape ? 30 : 7.5;
|
|
89
|
+
xMax = xMin + (boxWidthPercent * 100);
|
|
90
|
+
const containerWidth = Math.min(screenWidth * 0.95, 1000);
|
|
91
|
+
const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
|
|
92
|
+
const boxPixelWidth = containerWidth * boxWidthPercent;
|
|
93
|
+
const boxPixelHeight = boxPixelWidth / 1.59;
|
|
94
|
+
const heightPercent = (boxPixelHeight / containerHeight) * 100;
|
|
95
|
+
yMin = (100 - heightPercent) / 2.5;
|
|
96
|
+
yMax = yMin + heightPercent;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
xMin = 5;
|
|
100
|
+
xMax = 95;
|
|
101
|
+
yMin = 34;
|
|
102
|
+
yMax = 66;
|
|
103
|
+
}
|
|
104
|
+
const platformBbox = { xMin, yMin, xMax, yMax, borderColor: '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
|
|
76
105
|
return {
|
|
77
106
|
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
78
107
|
flashMode: 'auto',
|
|
79
|
-
overlay: {
|
|
80
|
-
guideText: instructions,
|
|
81
|
-
bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
|
|
82
|
-
}
|
|
108
|
+
overlay: { guideText: instructions, bbox: platformBbox }
|
|
83
109
|
};
|
|
84
|
-
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
110
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
|
|
85
111
|
const retakePicture = (sideToRetake) => {
|
|
86
112
|
setIsProcessingCapture(false);
|
|
87
113
|
setProcessingImagePath(null);
|
|
@@ -165,9 +191,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
165
191
|
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
166
192
|
};
|
|
167
193
|
try {
|
|
168
|
-
|
|
169
|
-
// NOTE: If you integrated the Kotlin centered crop function earlier, replace this with cropToCenterScanArea
|
|
170
|
-
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
|
|
194
|
+
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
|
|
171
195
|
}
|
|
172
196
|
catch (e) {
|
|
173
197
|
console.warn("Crop failed, falling back to original image", e);
|
|
@@ -184,6 +208,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
184
208
|
...newImages,
|
|
185
209
|
country: verified.country,
|
|
186
210
|
documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
|
|
211
|
+
...(verified.region ? { region: verified.region } : {})
|
|
187
212
|
});
|
|
188
213
|
}
|
|
189
214
|
setTimeout(() => {
|
|
@@ -215,9 +240,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
215
240
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
216
241
|
return;
|
|
217
242
|
}
|
|
243
|
+
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
218
244
|
try {
|
|
219
245
|
if (!templatePath) {
|
|
220
|
-
const templateType = await checkTemplateType({
|
|
246
|
+
const templateType = await checkTemplateType({
|
|
247
|
+
path: result.path || '',
|
|
248
|
+
docType: selectedDocumentType?.type,
|
|
249
|
+
docRegion: countryData?.code || "",
|
|
250
|
+
postfix: currentSide,
|
|
251
|
+
issuingSubdivision: issuingSubdivision
|
|
252
|
+
}, env);
|
|
221
253
|
templateResponse = templateType;
|
|
222
254
|
if (templateType.template_path)
|
|
223
255
|
templatePath = templateType.template_path;
|
|
@@ -240,19 +272,51 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
240
272
|
if (currentSide === 'front') {
|
|
241
273
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
242
274
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
243
|
-
verificationRes = await frontVerification({
|
|
275
|
+
verificationRes = await frontVerification({
|
|
276
|
+
path: result.path,
|
|
277
|
+
regionMapping: { authMethod: matchedAuthMethod ? [matchedAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes },
|
|
278
|
+
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType?.type] || '',
|
|
279
|
+
code: countryData?.code || '',
|
|
280
|
+
currentSide,
|
|
281
|
+
templatePath,
|
|
282
|
+
mrzType,
|
|
283
|
+
issuingSubdivision: issuingSubdivision
|
|
284
|
+
}, env);
|
|
244
285
|
}
|
|
245
286
|
else {
|
|
246
287
|
let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
247
288
|
if (!matchedBackAuthMethod && currentSide === 'back') {
|
|
248
289
|
matchedBackAuthMethod = 'MRZ';
|
|
249
290
|
}
|
|
250
|
-
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '
|
|
251
|
-
|
|
291
|
+
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
292
|
+
// 🚨 EXTRACT FRONT TEMPLATE TO VALIDATE AGAINST BACK TEMPLATE
|
|
293
|
+
const frontTemplatePath = capturedImages['front']?.templatePath || '';
|
|
294
|
+
verificationRes = await backVerification({
|
|
295
|
+
path: result.path,
|
|
296
|
+
regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes },
|
|
297
|
+
selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
|
|
298
|
+
code: countryData?.code || '',
|
|
299
|
+
currentSide,
|
|
300
|
+
templatePath,
|
|
301
|
+
mrzType: backMrzType,
|
|
302
|
+
templateResponse,
|
|
303
|
+
issuingSubdivision: issuingSubdivision,
|
|
304
|
+
frontTemplatePath: frontTemplatePath // 🚨 PASSED DOWN TO TRIGGER VALIDATION
|
|
305
|
+
}, env);
|
|
252
306
|
}
|
|
253
307
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
254
308
|
const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
255
|
-
const verifiedResult = {
|
|
309
|
+
const verifiedResult = {
|
|
310
|
+
path: result.path,
|
|
311
|
+
templatePath,
|
|
312
|
+
bbox,
|
|
313
|
+
success: true,
|
|
314
|
+
mrz,
|
|
315
|
+
isAnalyzing: false,
|
|
316
|
+
country: countryData?.code,
|
|
317
|
+
documentType: selectedDocumentType.type,
|
|
318
|
+
region: issuingSubdivision
|
|
319
|
+
};
|
|
256
320
|
setSilentCaptureResult(verifiedResult);
|
|
257
321
|
if (bbox)
|
|
258
322
|
setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
@@ -260,7 +324,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
260
324
|
}
|
|
261
325
|
catch (error) {
|
|
262
326
|
console.log("Backend Verification Error:", error);
|
|
263
|
-
// 2. Define a map of technical keywords to user-friendly messages
|
|
264
327
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
265
328
|
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
266
329
|
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
@@ -270,16 +333,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
270
333
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
271
334
|
}
|
|
272
335
|
else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
273
|
-
// 🚨 REMOVED 'too far' CHECK FROM HERE
|
|
274
336
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
275
337
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
success: false,
|
|
281
|
-
error: userFriendlyMessage
|
|
282
|
-
}));
|
|
338
|
+
else if (rawMessage.includes('ne correspond pas au recto')) { // 🚨 New Security Error Handler
|
|
339
|
+
userFriendlyMessage = 'The back of the document does not match the front. You must scan the same ID.';
|
|
340
|
+
}
|
|
341
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
283
342
|
}
|
|
284
343
|
}
|
|
285
344
|
};
|
|
@@ -293,48 +352,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
293
352
|
}, [showCamera]);
|
|
294
353
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
295
354
|
return (<View style={styles.root}>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
355
|
+
<View style={styles.previewContainer}>
|
|
356
|
+
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
357
|
+
<Text style={styles.description}>
|
|
358
|
+
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
359
|
+
</Text>
|
|
360
|
+
</View>
|
|
361
|
+
</View>);
|
|
303
362
|
}
|
|
304
|
-
// --- CAMERA RENDER ---
|
|
305
363
|
if (showCamera) {
|
|
306
364
|
const isBusy = isProcessingCapture;
|
|
307
365
|
if (isRebootingCamera) {
|
|
308
366
|
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
367
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
368
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
369
|
+
</View>);
|
|
312
370
|
}
|
|
313
371
|
return (<View style={styles.root}>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
<EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
|
|
337
|
-
<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success} language={state.currentLanguage} stepperProps={{
|
|
372
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
373
|
+
<View style={styles.headerContainer}>
|
|
374
|
+
<Text style={styles.headerTitle}>
|
|
375
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
376
|
+
</Text>
|
|
377
|
+
<View style={styles.stepBadge}>
|
|
378
|
+
<Text style={styles.stepText}>
|
|
379
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
380
|
+
</Text>
|
|
381
|
+
</View>
|
|
382
|
+
</View>
|
|
383
|
+
|
|
384
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
385
|
+
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
386
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
387
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
388
|
+
</TouchableOpacity>
|
|
389
|
+
</View>)}
|
|
390
|
+
|
|
391
|
+
<EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
|
|
392
|
+
<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success} language={state.currentLanguage} stepperProps={{
|
|
338
393
|
back: () => {
|
|
339
394
|
if (currentSide === 'back') {
|
|
340
395
|
setCurrentSide('front');
|
|
@@ -356,83 +411,76 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
356
411
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
357
412
|
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
358
413
|
}}/>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
</View>
|
|
408
|
-
</View>
|
|
409
|
-
</View>);
|
|
414
|
+
</>}/>
|
|
415
|
+
|
|
416
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
417
|
+
<View style={styles.topAnalyzingPill}>
|
|
418
|
+
<ActivityIndicator size="small" color="white"/>
|
|
419
|
+
<Text style={styles.analyzingPillText}>
|
|
420
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
421
|
+
</Text>
|
|
422
|
+
</View>
|
|
423
|
+
</View>)}
|
|
424
|
+
|
|
425
|
+
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
426
|
+
{processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
|
|
427
|
+
<View style={styles.processingOverlay}>
|
|
428
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
429
|
+
<Text style={styles.processingText}>
|
|
430
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
431
|
+
</Text>
|
|
432
|
+
</View>
|
|
433
|
+
</View>)}
|
|
434
|
+
|
|
435
|
+
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
436
|
+
{silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
|
|
437
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
438
|
+
</View>)}
|
|
439
|
+
<View style={styles.webActionButtonsRow}>
|
|
440
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
441
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
442
|
+
</TouchableOpacity>
|
|
443
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
444
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
445
|
+
</TouchableOpacity>
|
|
446
|
+
</View>
|
|
447
|
+
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
448
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
449
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
450
|
+
</TouchableOpacity>
|
|
451
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
452
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
453
|
+
</TouchableOpacity>
|
|
454
|
+
</View>) : null}
|
|
455
|
+
|
|
456
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
457
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
458
|
+
</View>) : null}
|
|
459
|
+
</View>
|
|
460
|
+
</View>
|
|
461
|
+
</View>);
|
|
410
462
|
}
|
|
411
|
-
// --- PREVIEW RENDER ---
|
|
412
463
|
return (<View style={styles.root}>
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
{capturedImages[currentSide]?.dir && (<>
|
|
434
|
-
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
435
|
-
<Button title={t('common.next')} onPress={() => {
|
|
464
|
+
<View style={styles.previewContainer}>
|
|
465
|
+
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
466
|
+
<View key={currentSide} style={styles.sideContainer}>
|
|
467
|
+
<Text style={styles.sideTitle}>
|
|
468
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
469
|
+
</Text>
|
|
470
|
+
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
471
|
+
{getLocalizedText(component.instructions)}
|
|
472
|
+
</Text>
|
|
473
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
474
|
+
<View style={styles.imagePreviewWrapper}>
|
|
475
|
+
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
476
|
+
</View>
|
|
477
|
+
{!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
|
|
478
|
+
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
|
|
479
|
+
{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/>)}
|
|
480
|
+
</View>)}
|
|
481
|
+
{capturedImages[currentSide]?.dir && (<>
|
|
482
|
+
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
483
|
+
<Button title={t('common.next')} onPress={() => {
|
|
436
484
|
if (!selectedDocumentType) {
|
|
437
485
|
showAlert('Error', 'Document type not selected');
|
|
438
486
|
return;
|
|
@@ -449,80 +497,67 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
449
497
|
setProcessingImagePath(null);
|
|
450
498
|
}
|
|
451
499
|
}} variant="primary" size="large" fullWidth/>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
500
|
+
</>)}
|
|
501
|
+
</View>
|
|
502
|
+
</View>
|
|
503
|
+
</ScrollView>
|
|
504
|
+
</View>
|
|
505
|
+
{showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
|
|
506
|
+
<View style={styles.qrModalContainer}>
|
|
507
|
+
<Text style={styles.qrModalTitle}>
|
|
508
|
+
{state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
|
|
509
|
+
</Text>
|
|
510
|
+
<Text style={styles.qrModalText}>
|
|
511
|
+
{state.currentLanguage === 'en' ? "Point your phone's camera at this QR code to seamlessly continue the process on your mobile device." : "Pointez l'appareil photo de votre téléphone vers ce code QR pour continuer le processus en toute fluidité sur votre appareil mobile."}
|
|
512
|
+
</Text>
|
|
513
|
+
<View style={styles.qrCodeWrapper}>
|
|
514
|
+
<QRCode value={currentUrl} size={220} backgroundColor="transparent"/>
|
|
515
|
+
</View>
|
|
516
|
+
<Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
|
|
517
|
+
</View>
|
|
518
|
+
</View>)}
|
|
519
|
+
</View>);
|
|
458
520
|
};
|
|
459
521
|
const styles = StyleSheet.create({
|
|
460
|
-
root: {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
})
|
|
465
|
-
},
|
|
466
|
-
cameraWrapper: {
|
|
467
|
-
width: '100%', backgroundColor: '#000000', overflow: 'hidden',
|
|
468
|
-
...Platform.select({
|
|
469
|
-
web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 },
|
|
470
|
-
default: { flex: 1 }
|
|
471
|
-
})
|
|
472
|
-
},
|
|
473
|
-
headerContainer: {
|
|
474
|
-
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
|
|
475
|
-
...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
|
|
476
|
-
},
|
|
477
|
-
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
|
|
522
|
+
root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
|
|
523
|
+
cameraWrapper: { width: '100%', backgroundColor: '#000000', overflow: 'hidden', ...Platform.select({ web: { maxWidth: 1000, width: '95%', height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 30, elevation: 24 }, default: { flex: 1 } }) },
|
|
524
|
+
headerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 10, paddingHorizontal: 20, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10, ...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } }) },
|
|
525
|
+
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
|
|
478
526
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
479
527
|
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
480
528
|
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
481
|
-
camera: { flex: 1, },
|
|
482
|
-
// MOBILE: Escape Hatch layout
|
|
483
529
|
escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
|
|
484
530
|
fallbackRefreshButton: { backgroundColor: 'rgba(0, 0, 0, 0.8)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.5)', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 5, elevation: 8 },
|
|
485
531
|
fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
|
|
532
|
+
webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 24, paddingBottom: 24, paddingTop: 30, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
|
|
533
|
+
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'center', gap: 16, marginTop: 12, maxWidth: 500, alignSelf: 'center', width: '100%' },
|
|
489
534
|
webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
490
535
|
webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
491
|
-
webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.
|
|
536
|
+
webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.15)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
492
537
|
webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
|
|
538
|
+
webTopControls: { position: 'absolute', top: Platform.OS === 'ios' ? 70 : 20, right: 20, zIndex: 9999 },
|
|
539
|
+
webFlipButton: { backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
|
|
496
540
|
webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
|
|
497
|
-
previewContainer: {
|
|
498
|
-
width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
|
|
499
|
-
...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' } })
|
|
500
|
-
},
|
|
541
|
+
previewContainer: { width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 30, paddingHorizontal: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 16, elevation: 8, ...Platform.select({ web: { alignSelf: 'center', maxWidth: 650 }, default: { margin: 10, width: '95%' } }) },
|
|
501
542
|
previewItemContainer: { flexGrow: 1, },
|
|
502
543
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
503
544
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
504
545
|
sideContainer: { marginBottom: 24 },
|
|
505
|
-
sideTitle: { fontSize:
|
|
506
|
-
imagePreviewWrapper: {
|
|
507
|
-
width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
|
|
508
|
-
...Platform.select({
|
|
509
|
-
web: { aspectRatio: 1.59, height: 'auto' },
|
|
510
|
-
default: { height: 220 }
|
|
511
|
-
})
|
|
512
|
-
},
|
|
546
|
+
sideTitle: { fontSize: 22, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
547
|
+
imagePreviewWrapper: { width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.12, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0', ...Platform.select({ web: { aspectRatio: 1.59, height: 'auto', maxWidth: 450, alignSelf: 'center' }, default: { height: 220 } }) },
|
|
513
548
|
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
514
|
-
floatingErrorBanner: {
|
|
515
|
-
backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
|
|
516
|
-
...Platform.select({
|
|
517
|
-
default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
|
|
518
|
-
})
|
|
519
|
-
},
|
|
549
|
+
floatingErrorBanner: { backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%', ...Platform.select({ web: { position: 'absolute', bottom: '100%', marginBottom: 16, maxWidth: 500, alignSelf: 'center', zIndex: 100000, boxShadow: '0 4px 12px rgba(0,0,0,0.15)' }, default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 } }) },
|
|
520
550
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
521
|
-
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.
|
|
551
|
+
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
|
|
522
552
|
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
523
553
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
524
|
-
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 :
|
|
525
|
-
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.
|
|
526
|
-
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
554
|
+
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
555
|
+
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.7)', paddingVertical: 10, paddingHorizontal: 20, borderRadius: 30, gap: 10, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 },
|
|
556
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
557
|
+
qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
|
|
558
|
+
qrModalContainer: { backgroundColor: '#FFFFFF', borderRadius: 24, padding: 32, width: '90%', maxWidth: 400, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.15, shadowRadius: 30, elevation: 24, },
|
|
559
|
+
qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
|
|
560
|
+
qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
|
|
561
|
+
qrCodeWrapper: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#E2E8F0', marginBottom: 24, }
|
|
527
562
|
});
|
|
528
563
|
//# sourceMappingURL=IDCardCapture.js.map
|