@sanctum-key/react-native-sdk 1.0.13 → 1.0.15
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/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +148 -8
- 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 +168 -115
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +0 -12
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +73 -98
- package/build/src/utils/cropByObb.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +190 -33
- package/src/components/KYCElements/IDCardCapture.tsx +253 -182
- package/src/utils/cropByObb.ts +90 -127
|
@@ -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 { GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
|
|
@@ -23,8 +23,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
23
23
|
const [silentCaptureResult, setSilentCaptureResult] = useState({ success: false, isAnalyzing: false });
|
|
24
24
|
const [isProcessingCapture, setIsProcessingCapture] = useState(false);
|
|
25
25
|
const [processingImagePath, setProcessingImagePath] = useState(null);
|
|
26
|
-
// 🚨 ADDED: Key to force camera re-mount
|
|
27
26
|
const [cameraKey, setCameraKey] = useState(0);
|
|
27
|
+
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
28
|
+
// Web specific state
|
|
29
|
+
const [cameraType, setCameraType] = useState('back');
|
|
28
30
|
const documentTypeMapping = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
|
|
29
31
|
const { actions, state, env } = useTemplateKYCFlowContext();
|
|
30
32
|
const getLocalizedText = (text) => {
|
|
@@ -44,7 +46,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
44
46
|
return { type: mappedType, region: countrySelectionData.region || 'root' };
|
|
45
47
|
}, [countrySelectionData, documentTypeMapping]);
|
|
46
48
|
const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
|
|
47
|
-
const [isRebootingCamera, setIsRebootingCamera] = useState(false);
|
|
48
49
|
const refreshCamera = () => {
|
|
49
50
|
setIsRebootingCamera(true);
|
|
50
51
|
setTimeout(() => {
|
|
@@ -52,6 +53,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
52
53
|
setIsRebootingCamera(false);
|
|
53
54
|
}, 500);
|
|
54
55
|
};
|
|
56
|
+
const toggleCameraLens = () => {
|
|
57
|
+
setCameraType(prev => prev === 'back' ? 'front' : 'back');
|
|
58
|
+
refreshCamera();
|
|
59
|
+
};
|
|
55
60
|
useEffect(() => {
|
|
56
61
|
if (value && Object.keys(value).length > 0) {
|
|
57
62
|
if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
|
|
@@ -69,15 +74,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
69
74
|
const cameraConfig = useMemo(() => {
|
|
70
75
|
const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
|
|
71
76
|
return {
|
|
72
|
-
cameraType: '
|
|
73
|
-
|
|
77
|
+
cameraType: Platform.OS === 'web' ? cameraType : 'back',
|
|
78
|
+
flashMode: 'auto',
|
|
79
|
+
overlay: {
|
|
80
|
+
guideText: instructions,
|
|
81
|
+
bbox: { xMin: 5, yMin: 15, xMax: 95, yMax: 85, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 }
|
|
82
|
+
}
|
|
74
83
|
};
|
|
75
|
-
}, [selectedDocumentType, locale, component.instructions]);
|
|
84
|
+
}, [selectedDocumentType, locale, component.instructions, cameraType]);
|
|
76
85
|
const retakePicture = (sideToRetake) => {
|
|
77
86
|
setIsProcessingCapture(false);
|
|
78
87
|
setProcessingImagePath(null);
|
|
79
88
|
setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
|
|
80
89
|
setShowCamera(true);
|
|
90
|
+
refreshCamera();
|
|
81
91
|
actions.showCustomStepper(false);
|
|
82
92
|
setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
|
|
83
93
|
if (value) {
|
|
@@ -147,13 +157,22 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
147
157
|
setProcessingImagePath(capturePath);
|
|
148
158
|
try {
|
|
149
159
|
let imagePathForUpload = capturePath;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
// 🚨 THE FIX: Ignore the backend's broken absolute pixels.
|
|
161
|
+
// Calculate the crop using the Green UI Frame percentages (0.0 to 1.0)
|
|
162
|
+
const overlayBbox = cameraConfig.overlay.bbox;
|
|
163
|
+
const uiCropBbox = {
|
|
164
|
+
minX: overlayBbox.xMin / 100,
|
|
165
|
+
minY: overlayBbox.yMin / 100,
|
|
166
|
+
width: (overlayBbox.xMax - overlayBbox.xMin) / 100,
|
|
167
|
+
height: (overlayBbox.yMax - overlayBbox.yMin) / 100,
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
// Apply the crop using the UI percentages and a tight 2% tolerance
|
|
171
|
+
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.warn("Crop failed, falling back to original image", e);
|
|
175
|
+
imagePathForUpload = capturePath; // Fallback to raw image if crop fails
|
|
157
176
|
}
|
|
158
177
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
159
178
|
const newImages = {
|
|
@@ -177,7 +196,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
177
196
|
}, 600);
|
|
178
197
|
}
|
|
179
198
|
catch (e) {
|
|
180
|
-
|
|
199
|
+
console.error("Backend Error:", e);
|
|
200
|
+
const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
|
|
201
|
+
showAlert('Error', friendlyError);
|
|
181
202
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
|
|
182
203
|
setIsProcessingCapture(false);
|
|
183
204
|
setProcessingImagePath(null);
|
|
@@ -239,9 +260,27 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
239
260
|
await autoCapture(result.path, verifiedResult);
|
|
240
261
|
}
|
|
241
262
|
catch (error) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
// 1. Keep the technical log for your debugging console
|
|
264
|
+
console.error("Backend Verification Error:", error);
|
|
265
|
+
// 2. Define a map of technical keywords to user-friendly messages
|
|
266
|
+
const rawMessage = (error?.message || '').toLowerCase();
|
|
267
|
+
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
268
|
+
if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
269
|
+
userFriendlyMessage = 'Document unreadable. Please hold the camera steady in good light.';
|
|
270
|
+
}
|
|
271
|
+
else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
272
|
+
userFriendlyMessage = 'The document does not match your selected country.';
|
|
273
|
+
}
|
|
274
|
+
else if (rawMessage.includes('too far') || rawMessage.includes('card_not_fully_in_frame')) {
|
|
275
|
+
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
276
|
+
}
|
|
277
|
+
// 3. Set ONLY the friendly message to the UI state
|
|
278
|
+
setSilentCaptureResult(prev => ({
|
|
279
|
+
...prev,
|
|
280
|
+
isAnalyzing: false,
|
|
281
|
+
success: false,
|
|
282
|
+
error: userFriendlyMessage
|
|
283
|
+
}));
|
|
245
284
|
}
|
|
246
285
|
}
|
|
247
286
|
};
|
|
@@ -268,29 +307,35 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
268
307
|
const isBusy = isProcessingCapture;
|
|
269
308
|
if (isRebootingCamera) {
|
|
270
309
|
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
311
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
312
|
+
</View>);
|
|
274
313
|
}
|
|
275
314
|
return (<View style={styles.root}>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</Text>
|
|
282
|
-
<View style={styles.stepBadge}>
|
|
283
|
-
<Text style={styles.stepText}>
|
|
284
|
-
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
315
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
316
|
+
|
|
317
|
+
<View style={styles.headerContainer}>
|
|
318
|
+
<Text style={styles.headerTitle}>
|
|
319
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
285
320
|
</Text>
|
|
321
|
+
<View style={styles.stepBadge}>
|
|
322
|
+
<Text style={styles.stepText}>
|
|
323
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
324
|
+
</Text>
|
|
325
|
+
</View>
|
|
286
326
|
</View>
|
|
287
|
-
</View>
|
|
288
|
-
|
|
289
|
-
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
290
327
|
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
329
|
+
|
|
330
|
+
{/* WEB ONLY: Flip Camera Top Button */}
|
|
331
|
+
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
332
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
333
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
334
|
+
</TouchableOpacity>
|
|
335
|
+
</View>)}
|
|
336
|
+
|
|
337
|
+
<EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
|
|
338
|
+
<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={{
|
|
294
339
|
back: () => {
|
|
295
340
|
if (currentSide === 'back') {
|
|
296
341
|
setCurrentSide('front');
|
|
@@ -312,47 +357,59 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
312
357
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
313
358
|
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
314
359
|
}}/>
|
|
315
|
-
|
|
316
|
-
|
|
360
|
+
</>}/>
|
|
317
361
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
362
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
363
|
+
<View style={styles.topAnalyzingPill}>
|
|
364
|
+
<ActivityIndicator size="small" color="white"/>
|
|
365
|
+
<Text style={styles.analyzingPillText}>
|
|
366
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
367
|
+
</Text>
|
|
368
|
+
</View>
|
|
369
|
+
</View>)}
|
|
326
370
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
371
|
+
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
372
|
+
{processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
|
|
373
|
+
<View style={styles.processingOverlay}>
|
|
374
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
375
|
+
<Text style={styles.processingText}>
|
|
376
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
377
|
+
</Text>
|
|
378
|
+
</View>
|
|
379
|
+
</View>)}
|
|
336
380
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
381
|
+
{/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
|
|
382
|
+
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
383
|
+
{silentCaptureResult.error ? (<View style={styles.floatingErrorBanner}>
|
|
384
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
385
|
+
</View>) : <View style={{ height: 10 }}/>}
|
|
342
386
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
387
|
+
<View style={styles.webActionButtonsRow}>
|
|
388
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
389
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
390
|
+
</TouchableOpacity>
|
|
391
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
392
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
393
|
+
</TouchableOpacity>
|
|
394
|
+
</View>
|
|
395
|
+
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
396
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
397
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
398
|
+
</TouchableOpacity>
|
|
399
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
400
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
401
|
+
</TouchableOpacity>
|
|
402
|
+
</View>) : null}
|
|
347
403
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
405
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
406
|
+
</View>) : null}
|
|
351
407
|
|
|
408
|
+
</View>
|
|
352
409
|
</View>
|
|
353
|
-
</View>
|
|
354
|
-
</View>);
|
|
410
|
+
</View>);
|
|
355
411
|
}
|
|
412
|
+
// --- PREVIEW RENDER ---
|
|
356
413
|
return (<View style={styles.root}>
|
|
357
414
|
<View style={styles.previewContainer}>
|
|
358
415
|
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
@@ -363,16 +420,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
363
420
|
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
364
421
|
{getLocalizedText(component.instructions)}
|
|
365
422
|
</Text>
|
|
423
|
+
|
|
366
424
|
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
367
425
|
{silentCaptureResult?.error === 'TOO_FAR_AWAY' && (<View style={styles.warningBanner}>
|
|
368
426
|
<Text style={styles.warningText}>
|
|
369
427
|
{state.currentLanguage === "en" ? "Move the document closer to the camera and place it on a flat surface." : "Veuillez rapprocher le document de la caméra et le poser à plat."}
|
|
370
428
|
</Text>
|
|
371
429
|
</View>)}
|
|
430
|
+
|
|
431
|
+
{/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
|
|
372
432
|
<View style={styles.imagePreviewWrapper}>
|
|
373
433
|
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
374
434
|
</View>
|
|
375
|
-
|
|
435
|
+
|
|
436
|
+
{!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/>)}
|
|
376
437
|
{capturedImages[currentSide]?.dir && (<>
|
|
377
438
|
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
378
439
|
<Button title={t('common.next')} onPress={() => {
|
|
@@ -385,6 +446,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
385
446
|
}
|
|
386
447
|
else {
|
|
387
448
|
setShowCamera(true);
|
|
449
|
+
refreshCamera();
|
|
388
450
|
setCurrentSide('back');
|
|
389
451
|
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
|
|
390
452
|
setIsProcessingCapture(false);
|
|
@@ -401,38 +463,63 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
401
463
|
const styles = StyleSheet.create({
|
|
402
464
|
root: {
|
|
403
465
|
flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
|
|
404
|
-
...
|
|
466
|
+
...Platform.select({
|
|
467
|
+
web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' },
|
|
468
|
+
})
|
|
405
469
|
},
|
|
406
470
|
cameraWrapper: {
|
|
407
|
-
width: '100%', backgroundColor: '#
|
|
408
|
-
...
|
|
471
|
+
width: '100%', backgroundColor: '#000000', overflow: 'hidden',
|
|
472
|
+
...Platform.select({
|
|
473
|
+
web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 },
|
|
474
|
+
default: { flex: 1 }
|
|
475
|
+
})
|
|
409
476
|
},
|
|
410
477
|
headerContainer: {
|
|
411
478
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
|
|
412
|
-
...
|
|
479
|
+
...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
|
|
413
480
|
},
|
|
414
481
|
headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
|
|
415
482
|
stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
|
|
416
483
|
stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
|
|
417
484
|
cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
|
|
418
485
|
camera: { flex: 1, },
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
},
|
|
422
|
-
|
|
486
|
+
// MOBILE: Escape Hatch layout
|
|
487
|
+
escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
|
|
488
|
+
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 },
|
|
489
|
+
fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
|
|
490
|
+
// WEB: Control Bar layout
|
|
491
|
+
webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingBottom: 20, paddingTop: 20, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
|
|
492
|
+
webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
|
|
493
|
+
webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
|
|
494
|
+
webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
495
|
+
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' },
|
|
496
|
+
webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
|
|
497
|
+
// WEB: Flip Controls
|
|
498
|
+
webTopControls: { position: 'absolute', top: 20, right: 20, zIndex: 9999 },
|
|
499
|
+
webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
|
|
500
|
+
webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
|
|
423
501
|
previewContainer: {
|
|
424
502
|
width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
|
|
425
|
-
...
|
|
503
|
+
...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' } })
|
|
426
504
|
},
|
|
427
505
|
previewItemContainer: { flexGrow: 1, },
|
|
428
506
|
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
|
|
429
507
|
description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
|
|
430
508
|
sideContainer: { marginBottom: 24 },
|
|
431
509
|
sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
|
|
432
|
-
imagePreviewWrapper: {
|
|
510
|
+
imagePreviewWrapper: {
|
|
511
|
+
width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
|
|
512
|
+
...Platform.select({
|
|
513
|
+
web: { aspectRatio: 1.59, height: 'auto' }, // 🚨 Perfect ID ratio on web
|
|
514
|
+
default: { height: 220 } // 🚨 Strict original height on mobile
|
|
515
|
+
})
|
|
516
|
+
},
|
|
433
517
|
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
434
518
|
floatingErrorBanner: {
|
|
435
|
-
|
|
519
|
+
backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
|
|
520
|
+
...Platform.select({
|
|
521
|
+
default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
|
|
522
|
+
})
|
|
436
523
|
},
|
|
437
524
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
438
525
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
@@ -442,40 +529,6 @@ const styles = StyleSheet.create({
|
|
|
442
529
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
443
530
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
444
531
|
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|
|
445
|
-
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
446
|
-
escapeHatchContainer: {
|
|
447
|
-
position: 'absolute',
|
|
448
|
-
bottom: 40,
|
|
449
|
-
left: 0,
|
|
450
|
-
right: 0,
|
|
451
|
-
alignItems: 'center',
|
|
452
|
-
justifyContent: 'center',
|
|
453
|
-
zIndex: 99999, // Guarantees it is the top-most element
|
|
454
|
-
},
|
|
455
|
-
fallbackRefreshButton: {
|
|
456
|
-
backgroundColor: 'rgba(0, 0, 0, 0.8)', // Darker so it's visible on white or black
|
|
457
|
-
borderWidth: 1,
|
|
458
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
459
|
-
paddingVertical: 12,
|
|
460
|
-
paddingHorizontal: 24,
|
|
461
|
-
borderRadius: 24,
|
|
462
|
-
shadowColor: '#000',
|
|
463
|
-
shadowOffset: { width: 0, height: 4 },
|
|
464
|
-
shadowOpacity: 0.3,
|
|
465
|
-
shadowRadius: 5,
|
|
466
|
-
elevation: 8,
|
|
467
|
-
},
|
|
468
|
-
fallbackRefreshText: {
|
|
469
|
-
color: '#FFFFFF',
|
|
470
|
-
fontWeight: 'bold',
|
|
471
|
-
fontSize: 16,
|
|
472
|
-
},
|
|
473
|
-
absoluteFillObject: {
|
|
474
|
-
position: 'absolute',
|
|
475
|
-
top: 0,
|
|
476
|
-
left: 0,
|
|
477
|
-
right: 0,
|
|
478
|
-
bottom: 0,
|
|
479
|
-
},
|
|
532
|
+
analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
|
|
480
533
|
});
|
|
481
534
|
//# sourceMappingURL=IDCardCapture.js.map
|