@sanctum-key/react-native-sdk 1.0.19 → 1.0.21
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/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 +166 -129
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/VerificationProgressTemplate.js +62 -40
- package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
- package/build/src/config/region_mapping.d.ts.map +1 -1
- package/build/src/config/region_mapping.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 +82 -14
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +11 -0
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +104 -29
- 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 +1 -1
- package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
- package/src/components/KYCElements/IDCardCapture.tsx +613 -556
- package/src/components/KYCElements/VerificationProgressTemplate.tsx +80 -71
- package/src/config/region_mapping.ts +2 -1
- package/src/hooks/useTemplateKYCFlow.tsx +14 -14
- package/src/modules/api/CardAuthentification.ts +147 -67
- package/src/modules/api/KYCService.ts +166 -43
- package/src/web/WebKYCEntry.tsx +42 -7
|
@@ -84,18 +84,14 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
84
84
|
const isLandscape = screenWidth > screenHeight;
|
|
85
85
|
let xMin, xMax, yMin, yMax;
|
|
86
86
|
if (Platform.OS === 'web') {
|
|
87
|
-
// 🚨 UI FIX: Differentiate Desktop Web (Landscape) vs Mobile Web (Portrait)
|
|
88
87
|
const boxWidthPercent = isLandscape ? 0.40 : 0.85;
|
|
89
|
-
// On wide desktop, shift slightly right. On mobile portrait, center perfectly.
|
|
90
88
|
xMin = isLandscape ? 30 : 7.5;
|
|
91
89
|
xMax = xMin + (boxWidthPercent * 100);
|
|
92
|
-
// Safely calculate height to guarantee a 1.59 aspect ratio
|
|
93
90
|
const containerWidth = Math.min(screenWidth * 0.95, 1000);
|
|
94
91
|
const containerHeight = isLandscape ? screenHeight * 0.80 : screenHeight * 0.90;
|
|
95
92
|
const boxPixelWidth = containerWidth * boxWidthPercent;
|
|
96
93
|
const boxPixelHeight = boxPixelWidth / 1.59;
|
|
97
94
|
const heightPercent = (boxPixelHeight / containerHeight) * 100;
|
|
98
|
-
// Center vertically
|
|
99
95
|
yMin = (100 - heightPercent) / 2.5;
|
|
100
96
|
yMax = yMin + heightPercent;
|
|
101
97
|
}
|
|
@@ -212,6 +208,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
212
208
|
...newImages,
|
|
213
209
|
country: verified.country,
|
|
214
210
|
documentType: GovernmentDocumentTypeBackend[verified.documentType] || '',
|
|
211
|
+
...(verified.region ? { region: verified.region } : {})
|
|
215
212
|
});
|
|
216
213
|
}
|
|
217
214
|
setTimeout(() => {
|
|
@@ -243,9 +240,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
243
240
|
setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: false, success: false, error: 'Document type not selected' }));
|
|
244
241
|
return;
|
|
245
242
|
}
|
|
243
|
+
const issuingSubdivision = selectedDocumentType.region !== 'root' ? selectedDocumentType.region : undefined;
|
|
246
244
|
try {
|
|
247
245
|
if (!templatePath) {
|
|
248
|
-
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);
|
|
249
253
|
templateResponse = templateType;
|
|
250
254
|
if (templateType.template_path)
|
|
251
255
|
templatePath = templateType.template_path;
|
|
@@ -268,7 +272,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
268
272
|
if (currentSide === 'front') {
|
|
269
273
|
const matchedAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'front');
|
|
270
274
|
const mrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
|
|
271
|
-
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);
|
|
272
285
|
}
|
|
273
286
|
else {
|
|
274
287
|
let matchedBackAuthMethod = getCorrespondingAuthMethod(templatePath, regionMappings.regionMapping, regionMappings.key || '', 'back');
|
|
@@ -276,11 +289,34 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
276
289
|
matchedBackAuthMethod = 'MRZ';
|
|
277
290
|
}
|
|
278
291
|
const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || '';
|
|
279
|
-
|
|
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);
|
|
280
306
|
}
|
|
281
307
|
const bbox = verificationRes?.bbox || templateBbox;
|
|
282
308
|
const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
|
|
283
|
-
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
|
+
};
|
|
284
320
|
setSilentCaptureResult(verifiedResult);
|
|
285
321
|
if (bbox)
|
|
286
322
|
setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
@@ -299,6 +335,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
299
335
|
else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
300
336
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
301
337
|
}
|
|
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
|
+
}
|
|
302
341
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: userFriendlyMessage }));
|
|
303
342
|
}
|
|
304
343
|
}
|
|
@@ -312,44 +351,44 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
312
351
|
actions.showCustomStepper(!showCamera);
|
|
313
352
|
}, [showCamera]);
|
|
314
353
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
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>
|
|
354
|
+
return (<View style={styles.root}>
|
|
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>
|
|
322
361
|
</View>);
|
|
323
362
|
}
|
|
324
363
|
if (showCamera) {
|
|
325
364
|
const isBusy = isProcessingCapture;
|
|
326
365
|
if (isRebootingCamera) {
|
|
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>
|
|
366
|
+
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
367
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
368
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
330
369
|
</View>);
|
|
331
370
|
}
|
|
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={<>
|
|
371
|
+
return (<View style={styles.root}>
|
|
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={<>
|
|
353
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={{
|
|
354
393
|
back: () => {
|
|
355
394
|
if (currentSide === 'back') {
|
|
@@ -371,76 +410,76 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
371
410
|
},
|
|
372
411
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
373
412
|
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
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>
|
|
413
|
+
}}/>
|
|
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>
|
|
422
461
|
</View>);
|
|
423
462
|
}
|
|
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/>
|
|
463
|
+
return (<View style={styles.root}>
|
|
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/>
|
|
444
483
|
<Button title={t('common.next')} onPress={() => {
|
|
445
484
|
if (!selectedDocumentType) {
|
|
446
485
|
showAlert('Error', 'Document type not selected');
|
|
@@ -457,32 +496,31 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
457
496
|
setIsProcessingCapture(false);
|
|
458
497
|
setProcessingImagePath(null);
|
|
459
498
|
}
|
|
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>)}
|
|
499
|
+
}} variant="primary" size="large" fullWidth/>
|
|
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>)}
|
|
480
519
|
</View>);
|
|
481
520
|
};
|
|
482
521
|
const styles = StyleSheet.create({
|
|
483
522
|
root: { flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center', ...Platform.select({ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center' }, }) },
|
|
484
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 } }) },
|
|
485
|
-
// 🚨 UI FIX: flexWrap handles overflowing text perfectly on narrow mobile screens
|
|
486
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' } }) },
|
|
487
525
|
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', flexShrink: 1 },
|
|
488
526
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
@@ -513,7 +551,6 @@ const styles = StyleSheet.create({
|
|
|
513
551
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, ...Platform.select({ web: { backdropFilter: 'blur(4px)' } }) },
|
|
514
552
|
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
515
553
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
516
|
-
// 🚨 UI FIX: Dropped the pill slightly lower so it never overlaps the flip lens button
|
|
517
554
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 75, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
518
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 },
|
|
519
556
|
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
|