@sanctum-key/react-native-sdk 1.0.12 → 1.0.14
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/EmailVerificationTemplate.d.ts.map +1 -1
- package/build/src/components/KYCElements/EmailVerificationTemplate.js +69 -37
- package/build/src/components/KYCElements/EmailVerificationTemplate.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +131 -105
- 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 +97 -65
- package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +1 -1
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/EmailVerificationTemplate.tsx +127 -95
- package/src/components/KYCElements/IDCardCapture.tsx +210 -173
- package/src/components/KYCElements/PhoneVerificationTemplate.tsx +185 -165
- package/src/modules/api/KYCService.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
3
3
|
import { showAlert } from '../../utils/platformAlert';
|
|
4
4
|
import { EnhancedCameraView } from '../EnhancedCameraView';
|
|
5
5
|
import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -29,8 +29,10 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
29
29
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
30
30
|
const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
|
|
31
31
|
|
|
32
|
-
// 🚨 ADDED: Key to force camera re-mount
|
|
33
32
|
const [cameraKey, setCameraKey] = useState(0);
|
|
33
|
+
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
34
|
+
// Web specific state
|
|
35
|
+
const [cameraType, setCameraType] = useState<'back' | 'front'>('back');
|
|
34
36
|
|
|
35
37
|
const documentTypeMapping: Record<string, GovernmentDocumentType> = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
|
|
36
38
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
@@ -54,18 +56,18 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
54
56
|
|
|
55
57
|
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
56
58
|
|
|
57
|
-
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
58
|
-
|
|
59
59
|
const refreshCamera = () => {
|
|
60
60
|
setIsRebootingCamera(true);
|
|
61
|
-
|
|
62
61
|
setTimeout(() => {
|
|
63
62
|
setCameraKey(prev => prev + 1);
|
|
64
63
|
setIsRebootingCamera(false);
|
|
65
64
|
}, 500);
|
|
66
65
|
};
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
const toggleCameraLens = () => {
|
|
68
|
+
setCameraType(prev => prev === 'back' ? 'front' : 'back');
|
|
69
|
+
refreshCamera();
|
|
70
|
+
};
|
|
69
71
|
|
|
70
72
|
useEffect(() => {
|
|
71
73
|
if (value && Object.keys(value).length > 0) {
|
|
@@ -85,16 +87,18 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
85
87
|
const cameraConfig = useMemo(() => {
|
|
86
88
|
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
|
|
87
89
|
return {
|
|
88
|
-
cameraType: '
|
|
90
|
+
cameraType: Platform.OS === 'web' ? cameraType : 'back', // Keep strictly 'back' on mobile
|
|
91
|
+
flashMode: 'auto' as const,
|
|
89
92
|
overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
|
|
90
93
|
};
|
|
91
|
-
}, [selectedDocumentType, locale, component.instructions]);
|
|
94
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
92
95
|
|
|
93
96
|
const retakePicture = (sideToRetake: 'front' | 'back') => {
|
|
94
97
|
setIsProcessingCapture(false);
|
|
95
98
|
setProcessingImagePath(null);
|
|
96
99
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
97
100
|
setShowCamera(true);
|
|
101
|
+
refreshCamera();
|
|
98
102
|
actions.showCustomStepper(false);
|
|
99
103
|
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
100
104
|
if (value) { const newValue = { ...value }; delete newValue[sideToRetake]; onValueChange(newValue); }
|
|
@@ -179,7 +183,9 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
179
183
|
setProcessingImagePath(null);
|
|
180
184
|
}, 600);
|
|
181
185
|
} catch (e: any) {
|
|
182
|
-
|
|
186
|
+
console.error("Backend Error:", e);
|
|
187
|
+
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
188
|
+
showAlert('Error', friendlyError);
|
|
183
189
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
184
190
|
setIsProcessingCapture(false);
|
|
185
191
|
setProcessingImagePath(null);
|
|
@@ -236,8 +242,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
236
242
|
if (bbox) setBboxBySide(prev => ({ ...prev, [currentSide]: bbox }));
|
|
237
243
|
await autoCapture(result.path, verifiedResult);
|
|
238
244
|
} catch (error: any) {
|
|
245
|
+
console.error("Backend Error:", error);
|
|
246
|
+
|
|
239
247
|
const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
|
|
240
|
-
|
|
248
|
+
|
|
249
|
+
const errorMessage = isCardNotFullyInFrame
|
|
250
|
+
? t('kyc.idCardCapture.cardNotFullyInFrame')
|
|
251
|
+
: (t('errors.genericVerificationFailed') || 'Verification failed. Please ensure the document is clear and try again.');
|
|
252
|
+
|
|
241
253
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: errorMessage }));
|
|
242
254
|
}
|
|
243
255
|
}
|
|
@@ -266,134 +278,157 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
266
278
|
);
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
// --- CAMERA RENDER ---
|
|
282
|
+
if (showCamera) {
|
|
283
|
+
const isBusy = isProcessingCapture;
|
|
269
284
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
if (isRebootingCamera) {
|
|
286
|
+
return (
|
|
287
|
+
<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
288
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
289
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
290
|
+
</View>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
273
293
|
|
|
274
|
-
if (isRebootingCamera) {
|
|
275
294
|
return (
|
|
276
|
-
<View style={
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<View style={styles.root}>
|
|
285
|
-
<View style={[styles.cameraWrapper, { flex: 1, minHeight: 400 }]}>
|
|
286
|
-
|
|
287
|
-
<View style={styles.headerContainer}>
|
|
288
|
-
<Text style={styles.headerTitle}>
|
|
289
|
-
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
290
|
-
</Text>
|
|
291
|
-
<View style={styles.stepBadge}>
|
|
292
|
-
<Text style={styles.stepText}>
|
|
293
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
295
|
+
<View style={styles.root}>
|
|
296
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
297
|
+
|
|
298
|
+
<View style={styles.headerContainer}>
|
|
299
|
+
<Text style={styles.headerTitle}>
|
|
300
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
294
301
|
</Text>
|
|
302
|
+
<View style={styles.stepBadge}>
|
|
303
|
+
<Text style={styles.stepText}>
|
|
304
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
305
|
+
</Text>
|
|
306
|
+
</View>
|
|
295
307
|
</View>
|
|
296
|
-
</View>
|
|
297
|
-
|
|
298
|
-
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
299
308
|
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
silentCaptureResult={silentCaptureResult}
|
|
309
|
-
overlayComponent={
|
|
310
|
-
<>
|
|
311
|
-
{/* We ONLY put the ID frame here, because if the camera fails, we don't care if the frame fails too */}
|
|
312
|
-
<IdCardOverlay
|
|
313
|
-
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
314
|
-
instructions={cameraConfig.overlay.guideText}
|
|
315
|
-
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
316
|
-
isSuccess={silentCaptureResult.success}
|
|
317
|
-
language={state.currentLanguage}
|
|
318
|
-
stepperProps={{
|
|
319
|
-
back: () => {
|
|
320
|
-
if (currentSide === 'back') {
|
|
321
|
-
setCurrentSide('front');
|
|
322
|
-
setShowCamera(false);
|
|
323
|
-
setIsProcessingCapture(false);
|
|
324
|
-
setProcessingImagePath(null);
|
|
325
|
-
if (capturedImages['front']?.dir) {
|
|
326
|
-
const frontImage = capturedImages['front'];
|
|
327
|
-
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
328
|
-
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
329
|
-
} else { actions.previousComponent(); }
|
|
330
|
-
},
|
|
331
|
-
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
332
|
-
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
333
|
-
}}
|
|
334
|
-
/>
|
|
335
|
-
</>
|
|
336
|
-
}
|
|
337
|
-
/>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
341
|
-
<View style={styles.topAnalyzingPillContainer}>
|
|
342
|
-
<View style={styles.topAnalyzingPill}>
|
|
343
|
-
<ActivityIndicator size="small" color="white" />
|
|
344
|
-
<Text style={styles.analyzingPillText}>
|
|
345
|
-
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
346
|
-
</Text>
|
|
309
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
310
|
+
|
|
311
|
+
{/* WEB ONLY: Flip Camera Top Button */}
|
|
312
|
+
{Platform.OS === 'web' && (
|
|
313
|
+
<View style={styles.webTopControls}>
|
|
314
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
315
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
316
|
+
</TouchableOpacity>
|
|
347
317
|
</View>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
<EnhancedCameraView
|
|
321
|
+
key={`${currentSide}-${cameraKey}`}
|
|
322
|
+
showCamera={true}
|
|
323
|
+
isProcessing={isBusy}
|
|
324
|
+
cameraType={cameraConfig.cameraType}
|
|
325
|
+
style={StyleSheet.absoluteFillObject}
|
|
326
|
+
onError={handleError}
|
|
327
|
+
onSilentCapture={handleSilentCapture}
|
|
328
|
+
silentCaptureResult={silentCaptureResult}
|
|
329
|
+
overlayComponent={
|
|
330
|
+
<>
|
|
331
|
+
<IdCardOverlay
|
|
332
|
+
xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax}
|
|
333
|
+
instructions={cameraConfig.overlay.guideText}
|
|
334
|
+
cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0 as number}
|
|
335
|
+
isSuccess={silentCaptureResult.success}
|
|
336
|
+
language={state.currentLanguage}
|
|
337
|
+
stepperProps={{
|
|
338
|
+
back: () => {
|
|
339
|
+
if (currentSide === 'back') {
|
|
340
|
+
setCurrentSide('front');
|
|
341
|
+
setShowCamera(false);
|
|
342
|
+
setIsProcessingCapture(false);
|
|
343
|
+
setProcessingImagePath(null);
|
|
344
|
+
if (capturedImages['front']?.dir) {
|
|
345
|
+
const frontImage = capturedImages['front'];
|
|
346
|
+
setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
|
|
347
|
+
} else { setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' })); }
|
|
348
|
+
} else { actions.previousComponent(); }
|
|
349
|
+
},
|
|
350
|
+
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
351
|
+
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
354
|
+
</>
|
|
355
|
+
}
|
|
356
|
+
/>
|
|
357
|
+
|
|
358
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (
|
|
359
|
+
<View style={styles.topAnalyzingPillContainer}>
|
|
360
|
+
<View style={styles.topAnalyzingPill}>
|
|
361
|
+
<ActivityIndicator size="small" color="white" />
|
|
362
|
+
<Text style={styles.analyzingPillText}>
|
|
363
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
364
|
+
</Text>
|
|
365
|
+
</View>
|
|
365
366
|
</View>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{isBusy && (
|
|
370
|
+
<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
371
|
+
{processingImagePath && (
|
|
372
|
+
<Image
|
|
373
|
+
source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }}
|
|
374
|
+
style={StyleSheet.absoluteFillObject}
|
|
375
|
+
resizeMode="cover"
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
378
|
+
<View style={styles.processingOverlay}>
|
|
379
|
+
<ActivityIndicator size="large" color="#2DBD60" />
|
|
380
|
+
<Text style={styles.processingText}>
|
|
381
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
382
|
+
</Text>
|
|
383
|
+
</View>
|
|
384
|
+
</View>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
{/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
|
|
388
|
+
{!isBusy && Platform.OS === 'web' ? (
|
|
389
|
+
<View style={styles.webBottomControlBar}>
|
|
390
|
+
{silentCaptureResult.error ? (
|
|
391
|
+
<View style={styles.floatingErrorBanner}>
|
|
392
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
393
|
+
</View>
|
|
394
|
+
) : <View style={{ height: 10 }} />}
|
|
395
|
+
|
|
396
|
+
<View style={styles.webActionButtonsRow}>
|
|
397
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
398
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
399
|
+
</TouchableOpacity>
|
|
400
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
401
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
402
|
+
</TouchableOpacity>
|
|
403
|
+
</View>
|
|
404
|
+
</View>
|
|
405
|
+
) : !isBusy ? (
|
|
406
|
+
<View style={styles.escapeHatchContainer}>
|
|
407
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
408
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
409
|
+
</TouchableOpacity>
|
|
410
|
+
<TouchableOpacity
|
|
411
|
+
style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]}
|
|
412
|
+
onPress={() => setShowCamera(false)}
|
|
413
|
+
>
|
|
414
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
415
|
+
</TouchableOpacity>
|
|
416
|
+
</View>
|
|
417
|
+
) : null}
|
|
418
|
+
|
|
419
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
|
|
420
|
+
<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
421
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
422
|
+
</View>
|
|
423
|
+
) : null}
|
|
390
424
|
|
|
425
|
+
</View>
|
|
391
426
|
</View>
|
|
392
427
|
</View>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
396
430
|
|
|
431
|
+
// --- PREVIEW RENDER ---
|
|
397
432
|
return (
|
|
398
433
|
<View style={styles.root}>
|
|
399
434
|
<View style={styles.previewContainer}>
|
|
@@ -405,6 +440,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
405
440
|
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
406
441
|
{getLocalizedText(component.instructions)}
|
|
407
442
|
</Text>
|
|
443
|
+
|
|
408
444
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
409
445
|
{silentCaptureResult?.error === 'TOO_FAR_AWAY' && (
|
|
410
446
|
<View style={styles.warningBanner}>
|
|
@@ -413,6 +449,8 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
413
449
|
</Text>
|
|
414
450
|
</View>
|
|
415
451
|
)}
|
|
452
|
+
|
|
453
|
+
{/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
|
|
416
454
|
<View style={styles.imagePreviewWrapper}>
|
|
417
455
|
{capturedImages[currentSide]?.dir ? (
|
|
418
456
|
<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage} />
|
|
@@ -420,10 +458,11 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
420
458
|
<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage} />
|
|
421
459
|
) : null}
|
|
422
460
|
</View>
|
|
461
|
+
|
|
423
462
|
{!capturedImages[currentSide]?.dir && (
|
|
424
463
|
<Button
|
|
425
464
|
title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
|
|
426
|
-
onPress={() => { setShowCamera(true); actions.showCustomStepper(false); }}
|
|
465
|
+
onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }}
|
|
427
466
|
variant="primary" size="large" fullWidth
|
|
428
467
|
/>
|
|
429
468
|
)}
|
|
@@ -436,6 +475,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
436
475
|
actions.nextComponent();
|
|
437
476
|
} else {
|
|
438
477
|
setShowCamera(true);
|
|
478
|
+
refreshCamera();
|
|
439
479
|
setCurrentSide('back');
|
|
440
480
|
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
441
481
|
setIsProcessingCapture(false);
|
|
@@ -455,38 +495,69 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
|
|
|
455
495
|
const styles = StyleSheet.create({
|
|
456
496
|
root: {
|
|
457
497
|
flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
|
|
458
|
-
...
|
|
498
|
+
...Platform.select({
|
|
499
|
+
web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } as any,
|
|
500
|
+
})
|
|
459
501
|
},
|
|
460
502
|
cameraWrapper: {
|
|
461
|
-
width: '100%', backgroundColor: '#
|
|
462
|
-
...
|
|
503
|
+
width: '100%', backgroundColor: '#000000', overflow: 'hidden',
|
|
504
|
+
...Platform.select({
|
|
505
|
+
web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 } as any,
|
|
506
|
+
default: { flex: 1 }
|
|
507
|
+
})
|
|
463
508
|
},
|
|
464
509
|
headerContainer: {
|
|
465
510
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
|
|
466
|
-
...
|
|
511
|
+
...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
|
|
467
512
|
},
|
|
468
513
|
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
|
|
469
514
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
470
515
|
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
471
516
|
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
472
517
|
camera: { flex: 1, },
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
},
|
|
476
|
-
|
|
518
|
+
|
|
519
|
+
// MOBILE: Escape Hatch layout
|
|
520
|
+
escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
|
|
521
|
+
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 },
|
|
522
|
+
fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
|
|
523
|
+
|
|
524
|
+
// WEB: Control Bar layout
|
|
525
|
+
webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingBottom: 20, paddingTop: 20, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
|
|
526
|
+
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
|
|
527
|
+
webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
528
|
+
webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
529
|
+
webSecondaryButton: { flex: 1, backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.4)', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
530
|
+
webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
531
|
+
|
|
532
|
+
// WEB: Flip Controls
|
|
533
|
+
webTopControls: { position: 'absolute', top: 20, right: 20, zIndex: 9999 },
|
|
534
|
+
webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
|
|
535
|
+
webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
|
|
536
|
+
|
|
477
537
|
previewContainer: {
|
|
478
538
|
width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
|
|
479
|
-
...
|
|
539
|
+
...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' }})
|
|
480
540
|
},
|
|
481
541
|
previewItemContainer: { flexGrow: 1, },
|
|
482
542
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
483
543
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
484
544
|
sideContainer: { marginBottom: 24 },
|
|
485
545
|
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
486
|
-
|
|
546
|
+
|
|
547
|
+
imagePreviewWrapper: {
|
|
548
|
+
width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
|
|
549
|
+
...Platform.select({
|
|
550
|
+
web: { aspectRatio: 1.59, height: 'auto' } as any, // 🚨 Perfect ID ratio on web
|
|
551
|
+
default: { height: 220 } // 🚨 Strict original height on mobile
|
|
552
|
+
})
|
|
553
|
+
},
|
|
487
554
|
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
555
|
+
|
|
488
556
|
floatingErrorBanner: {
|
|
489
|
-
|
|
557
|
+
backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
|
|
558
|
+
...Platform.select({
|
|
559
|
+
default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
|
|
560
|
+
})
|
|
490
561
|
},
|
|
491
562
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
492
563
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
@@ -496,39 +567,5 @@ const styles = StyleSheet.create({
|
|
|
496
567
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
497
568
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
498
569
|
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
499
|
-
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
500
|
-
escapeHatchContainer: {
|
|
501
|
-
position: 'absolute',
|
|
502
|
-
bottom: 40,
|
|
503
|
-
left: 0,
|
|
504
|
-
right: 0,
|
|
505
|
-
alignItems: 'center',
|
|
506
|
-
justifyContent: 'center',
|
|
507
|
-
zIndex: 99999, // Guarantees it is the top-most element
|
|
508
|
-
},
|
|
509
|
-
fallbackRefreshButton: {
|
|
510
|
-
backgroundColor: 'rgba(0, 0, 0, 0.8)', // Darker so it's visible on white or black
|
|
511
|
-
borderWidth: 1,
|
|
512
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
513
|
-
paddingVertical: 12,
|
|
514
|
-
paddingHorizontal: 24,
|
|
515
|
-
borderRadius: 24,
|
|
516
|
-
shadowColor: '#000',
|
|
517
|
-
shadowOffset: { width: 0, height: 4 },
|
|
518
|
-
shadowOpacity: 0.3,
|
|
519
|
-
shadowRadius: 5,
|
|
520
|
-
elevation: 8,
|
|
521
|
-
},
|
|
522
|
-
fallbackRefreshText: {
|
|
523
|
-
color: '#FFFFFF',
|
|
524
|
-
fontWeight: 'bold',
|
|
525
|
-
fontSize: 16,
|
|
526
|
-
},
|
|
527
|
-
absoluteFillObject: {
|
|
528
|
-
position: 'absolute',
|
|
529
|
-
top: 0,
|
|
530
|
-
left: 0,
|
|
531
|
-
right: 0,
|
|
532
|
-
bottom: 0,
|
|
533
|
-
},
|
|
570
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
534
571
|
});
|