@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.
@@ -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'; // 🚨 Added TouchableOpacity
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,17 @@ 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: 'back', flashMode: 'auto',
77
+ cameraType: Platform.OS === 'web' ? cameraType : 'back', // Keep strictly 'back' on mobile
78
+ flashMode: 'auto',
73
79
  overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
74
80
  };
75
- }, [selectedDocumentType, locale, component.instructions]);
81
+ }, [selectedDocumentType, locale, component.instructions, cameraType]);
76
82
  const retakePicture = (sideToRetake) => {
77
83
  setIsProcessingCapture(false);
78
84
  setProcessingImagePath(null);
79
85
  setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
80
86
  setShowCamera(true);
87
+ refreshCamera();
81
88
  actions.showCustomStepper(false);
82
89
  setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
83
90
  if (value) {
@@ -177,7 +184,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
177
184
  }, 600);
178
185
  }
179
186
  catch (e) {
180
- showAlert('Error', e?.message || 'Impossible de capturer la photo');
187
+ console.error("Backend Error:", e);
188
+ const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
189
+ showAlert('Error', friendlyError);
181
190
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
182
191
  setIsProcessingCapture(false);
183
192
  setProcessingImagePath(null);
@@ -239,8 +248,11 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
239
248
  await autoCapture(result.path, verifiedResult);
240
249
  }
241
250
  catch (error) {
251
+ console.error("Backend Error:", error);
242
252
  const isCardNotFullyInFrame = error?.message === 'CARD_NOT_FULLY_IN_FRAME' || error?.message?.includes('entirement');
243
- const errorMessage = isCardNotFullyInFrame ? t('kyc.idCardCapture.cardNotFullyInFrame') : (error?.message || 'Erreur de détection');
253
+ const errorMessage = isCardNotFullyInFrame
254
+ ? t('kyc.idCardCapture.cardNotFullyInFrame')
255
+ : (t('errors.genericVerificationFailed') || 'Verification failed. Please ensure the document is clear and try again.');
244
256
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: errorMessage }));
245
257
  }
246
258
  }
@@ -268,29 +280,35 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
268
280
  const isBusy = isProcessingCapture;
269
281
  if (isRebootingCamera) {
270
282
  return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
271
- <ActivityIndicator size="large" color="#2DBD60"/>
272
- <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
273
- </View>);
283
+ <ActivityIndicator size="large" color="#2DBD60"/>
284
+ <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
285
+ </View>);
274
286
  }
275
287
  return (<View style={styles.root}>
276
- <View style={[styles.cameraWrapper, { flex: 1, minHeight: 400 }]}>
277
-
278
- <View style={styles.headerContainer}>
279
- <Text style={styles.headerTitle}>
280
- {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
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' })}
288
+ <View style={[styles.cameraWrapper, { flex: 1 }]}>
289
+
290
+ <View style={styles.headerContainer}>
291
+ <Text style={styles.headerTitle}>
292
+ {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
285
293
  </Text>
294
+ <View style={styles.stepBadge}>
295
+ <Text style={styles.stepText}>
296
+ {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
297
+ </Text>
298
+ </View>
286
299
  </View>
287
- </View>
288
-
289
- <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
290
300
 
291
- <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={styles.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
292
- {/* We ONLY put the ID frame here, because if the camera fails, we don't care if the frame fails too */}
293
- <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={{
301
+ <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
302
+
303
+ {/* WEB ONLY: Flip Camera Top Button */}
304
+ {Platform.OS === 'web' && (<View style={styles.webTopControls}>
305
+ <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
306
+ <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
307
+ </TouchableOpacity>
308
+ </View>)}
309
+
310
+ <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
311
+ <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
312
  back: () => {
295
313
  if (currentSide === 'back') {
296
314
  setCurrentSide('front');
@@ -312,47 +330,59 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
312
330
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
313
331
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
314
332
  }}/>
315
- </>}/>
333
+ </>}/>
316
334
 
335
+ {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
336
+ <View style={styles.topAnalyzingPill}>
337
+ <ActivityIndicator size="small" color="white"/>
338
+ <Text style={styles.analyzingPillText}>
339
+ {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
340
+ </Text>
341
+ </View>
342
+ </View>)}
317
343
 
318
- {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
319
- <View style={styles.topAnalyzingPill}>
320
- <ActivityIndicator size="small" color="white"/>
321
- <Text style={styles.analyzingPillText}>
322
- {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
323
- </Text>
324
- </View>
325
- </View>)}
326
-
327
- {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
328
- {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
329
- <View style={styles.processingOverlay}>
330
- <ActivityIndicator size="large" color="#2DBD60"/>
331
- <Text style={styles.processingText}>
332
- {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
333
- </Text>
334
- </View>
335
- </View>)}
344
+ {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
345
+ {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
346
+ <View style={styles.processingOverlay}>
347
+ <ActivityIndicator size="large" color="#2DBD60"/>
348
+ <Text style={styles.processingText}>
349
+ {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
350
+ </Text>
351
+ </View>
352
+ </View>)}
336
353
 
337
- {!isBusy && (<View style={styles.escapeHatchContainer}>
338
- {/* Refresh Button */}
339
- <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
340
- <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
341
- </TouchableOpacity>
354
+ {/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
355
+ {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
356
+ {silentCaptureResult.error ? (<View style={styles.floatingErrorBanner}>
357
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
358
+ </View>) : <View style={{ height: 10 }}/>}
342
359
 
343
- <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
344
- <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
345
- </TouchableOpacity>
346
- </View>)}
360
+ <View style={styles.webActionButtonsRow}>
361
+ <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
362
+ <Text style={styles.webSecondaryButtonText}>Cancel</Text>
363
+ </TouchableOpacity>
364
+ <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
365
+ <Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
366
+ </TouchableOpacity>
367
+ </View>
368
+ </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
369
+ <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
370
+ <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
371
+ </TouchableOpacity>
372
+ <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
373
+ <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
374
+ </TouchableOpacity>
375
+ </View>) : null}
347
376
 
348
- {silentCaptureResult.error && !isBusy ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
349
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
350
- </View>) : null}
377
+ {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
378
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
379
+ </View>) : null}
351
380
 
381
+ </View>
352
382
  </View>
353
- </View>
354
- </View>);
383
+ </View>);
355
384
  }
385
+ // --- PREVIEW RENDER ---
356
386
  return (<View style={styles.root}>
357
387
  <View style={styles.previewContainer}>
358
388
  <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
@@ -363,16 +393,20 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
363
393
  <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
364
394
  {getLocalizedText(component.instructions)}
365
395
  </Text>
396
+
366
397
  <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
367
398
  {silentCaptureResult?.error === 'TOO_FAR_AWAY' && (<View style={styles.warningBanner}>
368
399
  <Text style={styles.warningText}>
369
400
  {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
401
  </Text>
371
402
  </View>)}
403
+
404
+ {/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
372
405
  <View style={styles.imagePreviewWrapper}>
373
406
  {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
374
407
  </View>
375
- {!capturedImages[currentSide]?.dir && (<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>)}
408
+
409
+ {!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
410
  {capturedImages[currentSide]?.dir && (<>
377
411
  <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
378
412
  <Button title={t('common.next')} onPress={() => {
@@ -385,6 +419,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
385
419
  }
386
420
  else {
387
421
  setShowCamera(true);
422
+ refreshCamera();
388
423
  setCurrentSide('back');
389
424
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
390
425
  setIsProcessingCapture(false);
@@ -401,38 +436,63 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
401
436
  const styles = StyleSheet.create({
402
437
  root: {
403
438
  flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
404
- ...(Platform.OS === 'web' ? { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } : {})
439
+ ...Platform.select({
440
+ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' },
441
+ })
405
442
  },
406
443
  cameraWrapper: {
407
- width: '100%', backgroundColor: '#FFFFFF', overflow: 'hidden',
408
- ...(Platform.OS === 'web' ? { maxWidth: 500, height: 700, maxHeight: '90vh', borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24, } : { flex: 1, })
444
+ width: '100%', backgroundColor: '#000000', overflow: 'hidden',
445
+ ...Platform.select({
446
+ web: { maxWidth: 550, height: '80vh', minHeight: 600, borderRadius: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 20 }, shadowOpacity: 0.25, shadowRadius: 35, elevation: 24 },
447
+ default: { flex: 1 }
448
+ })
409
449
  },
410
450
  headerContainer: {
411
451
  flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
412
- ...(Platform.OS !== 'web' ? { display: 'none' } : {})
452
+ ...Platform.select({ web: { display: 'flex' }, default: { display: 'none' } })
413
453
  },
414
454
  headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
415
455
  stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
416
456
  stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
417
457
  cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
418
458
  camera: { flex: 1, },
419
- refreshButton: {
420
- position: 'absolute', bottom: 100, alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 24, zIndex: 500
421
- },
422
- refreshButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
459
+ // MOBILE: Escape Hatch layout
460
+ escapeHatchContainer: { position: 'absolute', bottom: 40, left: 0, right: 0, alignItems: 'center', justifyContent: 'center', zIndex: 99999 },
461
+ 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 },
462
+ fallbackRefreshText: { color: '#FFFFFF', fontWeight: 'bold', fontSize: 16 },
463
+ // WEB: Control Bar layout
464
+ webBottomControlBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingBottom: 20, paddingTop: 20, backgroundColor: 'rgba(0,0,0,0.6)', zIndex: 99999 },
465
+ webActionButtonsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 12 },
466
+ webPrimaryButton: { flex: 1, backgroundColor: '#2DBD60', paddingVertical: 14, borderRadius: 12, alignItems: 'center' },
467
+ webPrimaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
468
+ 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' },
469
+ webSecondaryButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
470
+ // WEB: Flip Controls
471
+ webTopControls: { position: 'absolute', top: 20, right: 20, zIndex: 9999 },
472
+ webFlipButton: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' },
473
+ webFlipText: { color: 'white', fontWeight: 'bold', fontSize: 14 },
423
474
  previewContainer: {
424
475
  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
- ...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
476
+ ...Platform.select({ web: { alignSelf: 'center', maxWidth: 600 }, default: { margin: 10, width: '95%' } })
426
477
  },
427
478
  previewItemContainer: { flexGrow: 1, },
428
479
  title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
429
480
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
430
481
  sideContainer: { marginBottom: 24 },
431
482
  sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
432
- imagePreviewWrapper: { width: '100%', height: 220, borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0' },
483
+ imagePreviewWrapper: {
484
+ width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
485
+ ...Platform.select({
486
+ web: { aspectRatio: 1.59, height: 'auto' }, // 🚨 Perfect ID ratio on web
487
+ default: { height: 220 } // 🚨 Strict original height on mobile
488
+ })
489
+ },
433
490
  previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
434
491
  floatingErrorBanner: {
435
- position: 'absolute', bottom: 30, left: 24, right: 24, backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', shadowColor: '#DC2626', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 8, zIndex: 100
492
+ backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FCA5A5', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 12, alignItems: 'center', justifyContent: 'center', width: '100%',
493
+ ...Platform.select({
494
+ default: { position: 'absolute', top: Platform.OS === 'ios' ? 90 : 70, left: 24, right: 24, zIndex: 10000 }
495
+ })
436
496
  },
437
497
  floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
438
498
  processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
@@ -442,40 +502,6 @@ const styles = StyleSheet.create({
442
502
  errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
443
503
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
444
504
  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
- },
505
+ analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' }
480
506
  });
481
507
  //# sourceMappingURL=IDCardCapture.js.map