@sanctum-key/react-native-sdk 1.0.18 → 1.0.19
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/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +189 -191
- 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/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/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +0 -1
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +41 -24
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +3 -2
- package/src/components/EnhancedCameraView.tsx +28 -219
- package/src/components/KYCElements/IDCardCapture.tsx +560 -581
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
- package/src/components/OverLay/IdCard.tsx +48 -36
- package/src/config/countriesData.ts +0 -4
- package/src/modules/api/CardAuthentification.ts +0 -1
- package/src/modules/api/KYCService.ts +48 -29
|
@@ -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,37 @@ 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
|
+
// 🚨 UI FIX: Differentiate Desktop Web (Landscape) vs Mobile Web (Portrait)
|
|
88
|
+
const boxWidthPercent = isLandscape ? 0.40 : 0.85;
|
|
89
|
+
// On wide desktop, shift slightly right. On mobile portrait, center perfectly.
|
|
90
|
+
xMin = isLandscape ? 30 : 7.5;
|
|
91
|
+
xMax = xMin + (boxWidthPercent * 100);
|
|
92
|
+
// Safely calculate height to guarantee a 1.59 aspect ratio
|
|
93
|
+
const containerWidth = Math.min(screenWidth * 0.95, 1000);
|
|
94
|
+
const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
|
|
95
|
+
const boxPixelWidth = containerWidth * boxWidthPercent;
|
|
96
|
+
const boxPixelHeight = boxPixelWidth / 1.59;
|
|
97
|
+
const heightPercent = (boxPixelHeight / containerHeight) * 100;
|
|
98
|
+
// Center vertically
|
|
99
|
+
yMin = (100 - heightPercent) / 2.5;
|
|
100
|
+
yMax = yMin + heightPercent;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
xMin = 5;
|
|
104
|
+
xMax = 95;
|
|
105
|
+
yMin = 34;
|
|
106
|
+
yMax = 66;
|
|
107
|
+
}
|
|
108
|
+
const platformBbox = { xMin, yMin, xMax, yMax, borderColor: '#2DBD60', borderWidth: Platform.OS === 'web' ? 2 : 3, cornerRadius: 8 };
|
|
76
109
|
return {
|
|
77
110
|
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
78
111
|
flashMode: 'auto',
|
|
79
|
-
overlay: {
|
|
80
|
-
guideText: instructions,
|
|
81
|
-
bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
|
|
82
|
-
}
|
|
112
|
+
overlay: { guideText: instructions, bbox: platformBbox }
|
|
83
113
|
};
|
|
84
|
-
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
114
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight]);
|
|
85
115
|
const retakePicture = (sideToRetake) => {
|
|
86
116
|
setIsProcessingCapture(false);
|
|
87
117
|
setProcessingImagePath(null);
|
|
@@ -165,9 +195,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
165
195
|
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
166
196
|
};
|
|
167
197
|
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);
|
|
198
|
+
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.06);
|
|
171
199
|
}
|
|
172
200
|
catch (e) {
|
|
173
201
|
console.warn("Crop failed, falling back to original image", e);
|
|
@@ -247,7 +275,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
247
275
|
if (!matchedBackAuthMethod && currentSide === 'back') {
|
|
248
276
|
matchedBackAuthMethod = 'MRZ';
|
|
249
277
|
}
|
|
250
|
-
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '
|
|
278
|
+
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
251
279
|
verificationRes = await backVerification({ path: result.path, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '', code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse }, env);
|
|
252
280
|
}
|
|
253
281
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
@@ -260,7 +288,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
260
288
|
}
|
|
261
289
|
catch (error) {
|
|
262
290
|
console.log("Backend Verification Error:", error);
|
|
263
|
-
// 2. Define a map of technical keywords to user-friendly messages
|
|
264
291
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
265
292
|
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
266
293
|
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
@@ -270,16 +297,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
270
297
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
271
298
|
}
|
|
272
299
|
else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
273
|
-
// 🚨 REMOVED 'too far' CHECK FROM HERE
|
|
274
300
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
275
301
|
}
|
|
276
|
-
|
|
277
|
-
setSilentCaptureResult(prev => ({
|
|
278
|
-
...prev,
|
|
279
|
-
isAnalyzing: false,
|
|
280
|
-
success: false,
|
|
281
|
-
error: userFriendlyMessage
|
|
282
|
-
}));
|
|
302
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
283
303
|
}
|
|
284
304
|
}
|
|
285
305
|
};
|
|
@@ -292,49 +312,45 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
292
312
|
actions.showCustomStepper(!showCamera);
|
|
293
313
|
}, [showCamera]);
|
|
294
314
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
295
|
-
return (<View style={styles.root}>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
315
|
+
return (<View style={styles.root}>
|
|
316
|
+
<View style={styles.previewContainer}>
|
|
317
|
+
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
318
|
+
<Text style={styles.description}>
|
|
319
|
+
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
320
|
+
</Text>
|
|
321
|
+
</View>
|
|
322
|
+
</View>);
|
|
303
323
|
}
|
|
304
|
-
// --- CAMERA RENDER ---
|
|
305
324
|
if (showCamera) {
|
|
306
325
|
const isBusy = isProcessingCapture;
|
|
307
326
|
if (isRebootingCamera) {
|
|
308
|
-
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
327
|
+
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
328
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
329
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
330
|
+
</View>);
|
|
312
331
|
}
|
|
313
|
-
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={{
|
|
332
|
+
return (<View style={styles.root}>
|
|
333
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
334
|
+
<View style={styles.headerContainer}>
|
|
335
|
+
<Text style={styles.headerTitle}>
|
|
336
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
337
|
+
</Text>
|
|
338
|
+
<View style={styles.stepBadge}>
|
|
339
|
+
<Text style={styles.stepText}>
|
|
340
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
341
|
+
</Text>
|
|
342
|
+
</View>
|
|
343
|
+
</View>
|
|
344
|
+
|
|
345
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
346
|
+
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
347
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
348
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
349
|
+
</TouchableOpacity>
|
|
350
|
+
</View>)}
|
|
351
|
+
|
|
352
|
+
<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={<>
|
|
353
|
+
<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
354
|
back: () => {
|
|
339
355
|
if (currentSide === 'back') {
|
|
340
356
|
setCurrentSide('front');
|
|
@@ -355,84 +371,77 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
355
371
|
},
|
|
356
372
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
357
373
|
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
358
|
-
}}/>
|
|
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>);
|
|
374
|
+
}}/>
|
|
375
|
+
</>}/>
|
|
376
|
+
|
|
377
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
378
|
+
<View style={styles.topAnalyzingPill}>
|
|
379
|
+
<ActivityIndicator size="small" color="white"/>
|
|
380
|
+
<Text style={styles.analyzingPillText}>
|
|
381
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
382
|
+
</Text>
|
|
383
|
+
</View>
|
|
384
|
+
</View>)}
|
|
385
|
+
|
|
386
|
+
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
387
|
+
{processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
|
|
388
|
+
<View style={styles.processingOverlay}>
|
|
389
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
390
|
+
<Text style={styles.processingText}>
|
|
391
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
392
|
+
</Text>
|
|
393
|
+
</View>
|
|
394
|
+
</View>)}
|
|
395
|
+
|
|
396
|
+
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
397
|
+
{silentCaptureResult.error && (<View style={styles.floatingErrorBanner}>
|
|
398
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
399
|
+
</View>)}
|
|
400
|
+
<View style={styles.webActionButtonsRow}>
|
|
401
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
402
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
403
|
+
</TouchableOpacity>
|
|
404
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
405
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
406
|
+
</TouchableOpacity>
|
|
407
|
+
</View>
|
|
408
|
+
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
409
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
410
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
411
|
+
</TouchableOpacity>
|
|
412
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
413
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
414
|
+
</TouchableOpacity>
|
|
415
|
+
</View>) : null}
|
|
416
|
+
|
|
417
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
418
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
419
|
+
</View>) : null}
|
|
420
|
+
</View>
|
|
421
|
+
</View>
|
|
422
|
+
</View>);
|
|
410
423
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
{!capturedImages[currentSide]?.dir && (<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>)}
|
|
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={() => {
|
|
424
|
+
return (<View style={styles.root}>
|
|
425
|
+
<View style={styles.previewContainer}>
|
|
426
|
+
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
427
|
+
<View key={currentSide} style={styles.sideContainer}>
|
|
428
|
+
<Text style={styles.sideTitle}>
|
|
429
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
430
|
+
</Text>
|
|
431
|
+
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
432
|
+
{getLocalizedText(component.instructions)}
|
|
433
|
+
</Text>
|
|
434
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
435
|
+
<View style={styles.imagePreviewWrapper}>
|
|
436
|
+
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
437
|
+
</View>
|
|
438
|
+
{!capturedImages[currentSide]?.dir && (<View style={{ width: '100%', gap: 12 }}>
|
|
439
|
+
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>
|
|
440
|
+
{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/>)}
|
|
441
|
+
</View>)}
|
|
442
|
+
{capturedImages[currentSide]?.dir && (<>
|
|
443
|
+
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
444
|
+
<Button title={t('common.next')} onPress={() => {
|
|
436
445
|
if (!selectedDocumentType) {
|
|
437
446
|
showAlert('Error', 'Document type not selected');
|
|
438
447
|
return;
|
|
@@ -448,81 +457,70 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
448
457
|
setIsProcessingCapture(false);
|
|
449
458
|
setProcessingImagePath(null);
|
|
450
459
|
}
|
|
451
|
-
}} variant="primary" size="large" fullWidth/>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
460
|
+
}} variant="primary" size="large" fullWidth/>
|
|
461
|
+
</>)}
|
|
462
|
+
</View>
|
|
463
|
+
</View>
|
|
464
|
+
</ScrollView>
|
|
465
|
+
</View>
|
|
466
|
+
{showQRModal && Platform.OS === 'web' && (<View style={styles.qrModalOverlay}>
|
|
467
|
+
<View style={styles.qrModalContainer}>
|
|
468
|
+
<Text style={styles.qrModalTitle}>
|
|
469
|
+
{state.currentLanguage === 'en' ? 'Scan to continue' : 'Scannez pour continuer'}
|
|
470
|
+
</Text>
|
|
471
|
+
<Text style={styles.qrModalText}>
|
|
472
|
+
{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."}
|
|
473
|
+
</Text>
|
|
474
|
+
<View style={styles.qrCodeWrapper}>
|
|
475
|
+
<QRCode value={currentUrl} size={220} backgroundColor="transparent"/>
|
|
476
|
+
</View>
|
|
477
|
+
<Button title={state.currentLanguage === 'en' ? "Close" : "Fermer"} onPress={() => setShowQRModal(false)} variant="outline" fullWidth/>
|
|
478
|
+
</View>
|
|
479
|
+
</View>)}
|
|
480
|
+
</View>);
|
|
458
481
|
};
|
|
459
482
|
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', },
|
|
483
|
+
root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
|
|
484
|
+
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 } }) },
|
|
485
|
+
// 🚨 UI FIX: flexWrap handles overflowing text perfectly on narrow mobile screens
|
|
486
|
+
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' } }) },
|
|
487
|
+
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
|
|
478
488
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
479
489
|
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
480
490
|
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
481
|
-
camera: { flex: 1, },
|
|
482
|
-
// MOBILE: Escape Hatch layout
|
|
483
491
|
escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
|
|
484
492
|
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
493
|
fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
|
|
494
|
+
webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 24, paddingBottom: 24, paddingTop: 30, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
|
|
495
|
+
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'center', gap: 16, marginTop: 12, maxWidth: 500, alignSelf: 'center', width: '100%' },
|
|
489
496
|
webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
490
497
|
webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
491
|
-
webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.
|
|
498
|
+
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
499
|
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)' },
|
|
500
|
+
webTopControls: { position: 'absolute', top: Platform.OS === 'ios' ? 70 : 20, right: 20, zIndex: 9999 },
|
|
501
|
+
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
502
|
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
|
-
},
|
|
503
|
+
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
504
|
previewItemContainer: { flexGrow: 1, },
|
|
502
505
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
503
506
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
504
507
|
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
|
-
},
|
|
508
|
+
sideTitle: { fontSize: 22, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
509
|
+
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
510
|
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
|
-
},
|
|
511
|
+
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
512
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
521
|
-
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.
|
|
513
|
+
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
|
|
522
514
|
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
523
515
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
516
|
+
// 🚨 UI FIX: Dropped the pill slightly lower so it never overlaps the flip lens button
|
|
517
|
+
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
518
|
+
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 },
|
|
519
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|
|
520
|
+
qrModalOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 999999, ...Platform.select({ web: { backdropFilter: 'blur(5px)' } }) },
|
|
521
|
+
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, },
|
|
522
|
+
qrModalTitle: { fontSize: 22, fontWeight: 'bold', color: '#0F172A', marginBottom: 12, textAlign: 'center' },
|
|
523
|
+
qrModalText: { fontSize: 15, color: '#64748B', textAlign: 'center', marginBottom: 24, lineHeight: 22, },
|
|
524
|
+
qrCodeWrapper: { padding: 16, backgroundColor: '#FFFFFF', borderRadius: 16, borderWidth: 1, borderColor: '#E2E8F0', marginBottom: 24, }
|
|
527
525
|
});
|
|
528
526
|
//# sourceMappingURL=IDCardCapture.js.map
|