@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.
@@ -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,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: 'back', flashMode: 'auto',
73
- overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
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
- if (verified.bbox) {
151
- try {
152
- imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, verified.bbox, 0.15);
153
- }
154
- catch {
155
- imagePathForUpload = capturePath;
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
- showAlert('Error', e?.message || 'Impossible de capturer la photo');
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
- 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');
244
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: errorMessage }));
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
- <ActivityIndicator size="large" color="#2DBD60"/>
272
- <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
273
- </View>);
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
- <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' })}
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
- <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={{
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
- {!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>)}
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
- {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>)}
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
- {!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>
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
- <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>)}
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
- {silentCaptureResult.error && !isBusy ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
349
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
350
- </View>) : null}
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
- {!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/>)}
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
- ...(Platform.OS === 'web' ? { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } : {})
466
+ ...Platform.select({
467
+ web: { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' },
468
+ })
405
469
  },
406
470
  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, })
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
- ...(Platform.OS !== 'web' ? { display: 'none' } : {})
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
- 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 },
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
- ...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
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: { 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' },
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
- 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
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