@sanctum-key/react-native-sdk 1.0.11 → 1.0.13

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';
2
+ import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity } from 'react-native'; // 🚨 Added TouchableOpacity
3
3
  import { showAlert } from '../../utils/platformAlert';
4
4
  import { EnhancedCameraView } from '../EnhancedCameraView';
5
5
  import { GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
@@ -13,11 +13,7 @@ import { getDocumentTypeInfo } from '../../utils/get-document-type-info';
13
13
  import pathToBase64 from '../../utils/pathToBase64';
14
14
  import { cropByObb, cropImageWithBBoxWithTolerance, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../utils/cropByObb';
15
15
  import REGION_MAPPING from '../../config/region_mapping.json';
16
- const ISO_TO_COUNTRY_NAME = {
17
- 'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
18
- 'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
19
- 'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
20
- };
16
+ const ISO_TO_COUNTRY_NAME = { 'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada', 'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain', 'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali' };
21
17
  export const IDCardCapture = ({ component, value = {}, onValueChange, error, language = 'en' }) => {
22
18
  const { t, locale } = useI18n();
23
19
  const [showCamera, setShowCamera] = useState(false);
@@ -27,20 +23,15 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
27
23
  const [silentCaptureResult, setSilentCaptureResult] = useState({ success: false, isAnalyzing: false });
28
24
  const [isProcessingCapture, setIsProcessingCapture] = useState(false);
29
25
  const [processingImagePath, setProcessingImagePath] = useState(null);
26
+ // 🚨 ADDED: Key to force camera re-mount
30
27
  const [cameraKey, setCameraKey] = useState(0);
31
- const documentTypeMapping = {
32
- 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence',
33
- 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card',
34
- };
28
+ const documentTypeMapping = { 'nationalId': 'national_id', 'passport': 'passport', 'driversLicense': 'drivers_licence', 'residencePermit': 'permanent_residence', 'healthInsuranceCard': 'health_insurance_card', };
35
29
  const { actions, state, env } = useTemplateKYCFlowContext();
36
30
  const getLocalizedText = (text) => {
37
31
  if (text && typeof text[currentSide] === 'object' && text[currentSide][locale])
38
32
  return text[currentSide][locale] || '';
39
33
  return "";
40
34
  };
41
- const refreshCamera = () => {
42
- setCameraKey(prev => prev + 1);
43
- };
44
35
  const countrySelectionData = useMemo(() => {
45
36
  const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
46
37
  return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
@@ -53,6 +44,14 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
53
44
  return { type: mappedType, region: countrySelectionData.region || 'root' };
54
45
  }, [countrySelectionData, documentTypeMapping]);
55
46
  const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
47
+ const [isRebootingCamera, setIsRebootingCamera] = useState(false);
48
+ const refreshCamera = () => {
49
+ setIsRebootingCamera(true);
50
+ setTimeout(() => {
51
+ setCameraKey(prev => prev + 1);
52
+ setIsRebootingCamera(false);
53
+ }, 500);
54
+ };
56
55
  useEffect(() => {
57
56
  if (value && Object.keys(value).length > 0) {
58
57
  if (JSON.stringify(value) !== JSON.stringify(capturedImages)) {
@@ -61,21 +60,14 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
61
60
  const currentImageData = updatedImages[currentSide];
62
61
  if (currentImageData?.dir) {
63
62
  setSilentCaptureResult(prev => ({
64
- ...prev,
65
- path: currentImageData.dir,
66
- success: true,
67
- isAnalyzing: false,
68
- mrz: currentImageData.mrz || '',
69
- templatePath: currentImageData.templatePath || '',
63
+ ...prev, path: currentImageData.dir, success: true, isAnalyzing: false, mrz: currentImageData.mrz || '', templatePath: currentImageData.templatePath || '',
70
64
  }));
71
65
  }
72
66
  }
73
67
  }
74
68
  }, [value, currentSide]);
75
69
  const cameraConfig = useMemo(() => {
76
- const instructions = selectedDocumentType
77
- ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
78
- : getLocalizedText(component.instructions);
70
+ const instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
79
71
  return {
80
72
  cameraType: 'back', flashMode: 'auto',
81
73
  overlay: { guideText: instructions, bbox: { xMin: 15, yMin: 20, xMax: 85, yMax: 70, borderColor: '#2DBD60', borderWidth: 3, cornerRadius: 8 } }
@@ -102,7 +94,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
102
94
  const rawCountryName = ISO_TO_COUNTRY_NAME[countryData?.code || ''] || countryData?.code || countryKey;
103
95
  const baseMapping = REGION_MAPPING.regionMapping || REGION_MAPPING;
104
96
  let countryMapping = baseMapping[rawCountryName];
105
- // Fallback search in case of case mismatches
106
97
  if (!countryMapping) {
107
98
  const foundKey = Object.keys(baseMapping).find(k => k.toLowerCase() === rawCountryName.toLowerCase() || k.toLowerCase() === countryKey.toLowerCase());
108
99
  if (foundKey)
@@ -197,7 +188,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
197
188
  return;
198
189
  if (result.success && result.path) {
199
190
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
200
- // 🚨 Force a template fetch if we haven't successfully saved the current side yet
201
191
  let templatePath = capturedImages[currentSide]?.templatePath || '';
202
192
  let templateBbox;
203
193
  let templateResponse;
@@ -238,19 +228,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
238
228
  matchedBackAuthMethod = 'MRZ';
239
229
  }
240
230
  const backMrzType = getCorrespondingMrzType(templatePath, regionMappings.regionMapping, regionMappings.key || '') || 'TD1';
241
- verificationRes = await backVerification({
242
- path: result.path,
243
- regionMapping: {
244
- authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod,
245
- mrzTypes: regionMappings.mrzTypes
246
- },
247
- selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '',
248
- code: countryData?.code || '',
249
- currentSide,
250
- templatePath,
251
- mrzType: backMrzType,
252
- templateResponse
253
- }, env);
231
+ verificationRes = await backVerification({ path: result.path, regionMapping: { authMethod: matchedBackAuthMethod ? [matchedBackAuthMethod] : regionMappings.authMethod, mrzTypes: regionMappings.mrzTypes }, selectedDocumentType: GovernmentDocumentTypeShorted[selectedDocumentType.type] || '', code: countryData?.code || '', currentSide, templatePath, mrzType: backMrzType, templateResponse }, env);
254
232
  }
255
233
  const bbox = verificationRes?.bbox || templateBbox;
256
234
  const mrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
@@ -272,7 +250,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
272
250
  setShowCamera(false);
273
251
  setIsProcessingCapture(false);
274
252
  };
275
- useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
253
+ useEffect(() => {
254
+ actions.showCustomStepper(!showCamera);
255
+ }, [showCamera]);
276
256
  if (!countrySelectionData || !selectedDocumentType) {
277
257
  return (<View style={styles.root}>
278
258
  <View style={styles.previewContainer}>
@@ -286,41 +266,31 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
286
266
  // --- CAMERA RENDER ---
287
267
  if (showCamera) {
288
268
  const isBusy = isProcessingCapture;
269
+ if (isRebootingCamera) {
270
+ 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>);
274
+ }
289
275
  return (<View style={styles.root}>
290
- <View style={styles.cameraWrapper}>
291
-
292
- {/* Web/Desktop Clean Header */}
293
- <View style={styles.headerContainer}>
294
- <Text style={styles.headerTitle}>
295
- {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
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' })}
296
285
  </Text>
297
- <View style={styles.stepBadge}>
298
- <Text style={styles.stepText}>
299
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
300
- </Text>
301
- </View>
302
286
  </View>
303
-
304
- <View style={styles.cameraFeedContainer}>
305
- <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={styles.camera} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
306
- {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
307
- <View style={styles.topAnalyzingPill}>
308
- <ActivityIndicator size="small" color="white"/>
309
- <Text style={styles.analyzingPillText}>
310
- {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
311
- </Text>
312
- </View>
313
- </View>)}
314
- {isBusy && (<View style={StyleSheet.absoluteFillObject}>
315
- {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
316
- <View style={styles.processingOverlay}>
317
- <ActivityIndicator size="large" color="#2DBD60"/>
318
- <Text style={styles.processingText}>
319
- {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
320
- </Text>
321
- </View>
322
- </View>)}
323
- <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={{
287
+ </View>
288
+
289
+ <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
290
+
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={{
324
294
  back: () => {
325
295
  if (currentSide === 'back') {
326
296
  setCurrentSide('front');
@@ -342,18 +312,46 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
342
312
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
343
313
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
344
314
  }}/>
315
+ </>}/>
316
+
317
+
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>)}
336
+
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>
342
+
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>)}
347
+
348
+ {silentCaptureResult.error && !isBusy ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
349
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
350
+ </View>) : null}
345
351
 
346
- <TouchableOpacity style={styles.refreshButton} onPress={refreshCamera}>
347
- <Text style={styles.refreshButtonText}>Refresh Camera</Text>
348
- </TouchableOpacity>
349
- </>}/>
350
- {/* Elegant Floating Error Banner below the cutout */}
351
- {silentCaptureResult.error && !isBusy ? (<View style={styles.floatingErrorBanner}>
352
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
353
- </View>) : null}
354
- </View>
355
352
  </View>
356
- </View>);
353
+ </View>
354
+ </View>);
357
355
  }
358
356
  return (<View style={styles.root}>
359
357
  <View style={styles.previewContainer}>
@@ -402,121 +400,39 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
402
400
  };
403
401
  const styles = StyleSheet.create({
404
402
  root: {
405
- flex: 1,
406
- width: '100%',
407
- backgroundColor: 'transparent',
408
- alignSelf: 'center',
409
- ...(Platform.OS === 'web'
410
- ? {
411
- minHeight: '85vh',
412
- justifyContent: 'center',
413
- alignItems: 'center',
414
- // Note: backdropFilter is valid in React Native Web but TS might complain, cast safely
415
- backdropFilter: 'blur(8px)'
416
- }
417
- : {})
403
+ flex: 1, width: '100%', backgroundColor: 'transparent', alignSelf: 'center',
404
+ ...(Platform.OS === 'web' ? { minHeight: '85vh', justifyContent: 'center', alignItems: 'center', backdropFilter: 'blur(8px)' } : {})
418
405
  },
419
406
  cameraWrapper: {
420
- width: '100%',
421
- backgroundColor: '#FFFFFF',
422
- overflow: 'hidden',
423
- ...(Platform.OS === 'web'
424
- ? {
425
- maxWidth: 500,
426
- height: 700,
427
- maxHeight: '90vh', // TypeScript will now ignore this thanks to the cast below
428
- borderRadius: 24,
429
- shadowColor: '#000',
430
- shadowOffset: { width: 0, height: 20 },
431
- shadowOpacity: 0.25,
432
- shadowRadius: 35,
433
- elevation: 24,
434
- } // 🚨 CAST TO ANY
435
- : {
436
- flex: 1,
437
- })
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, })
438
409
  },
439
410
  headerContainer: {
440
- flexDirection: 'row',
441
- alignItems: 'center',
442
- justifyContent: 'space-between',
443
- paddingHorizontal: 24,
444
- paddingVertical: 18,
445
- backgroundColor: '#FFFFFF',
446
- borderBottomWidth: 1,
447
- borderBottomColor: '#F1F5F9',
448
- zIndex: 10,
449
- // Mobile hidden, Web visible to replace floating text
411
+ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 18, backgroundColor: '#FFFFFF', borderBottomWidth: 1, borderBottomColor: '#F1F5F9', zIndex: 10,
450
412
  ...(Platform.OS !== 'web' ? { display: 'none' } : {})
451
413
  },
452
- headerTitle: {
453
- fontSize: 18,
454
- fontWeight: '700',
455
- color: '#0F172A',
456
- },
457
- stepBadge: {
458
- backgroundColor: '#F1F5F9',
459
- paddingHorizontal: 12,
460
- paddingVertical: 6,
461
- borderRadius: 20,
462
- },
463
- stepText: {
464
- fontSize: 13,
465
- fontWeight: '600',
466
- color: '#64748B',
467
- },
468
- cameraFeedContainer: {
469
- flex: 1,
470
- position: 'relative',
471
- backgroundColor: '#000',
472
- },
473
- camera: {
474
- flex: 1,
414
+ headerTitle: { fontSize: 18, fontWeight: '700', color: '#0F172A', },
415
+ stepBadge: { backgroundColor: '#F1F5F9', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20, },
416
+ stepText: { fontSize: 13, fontWeight: '600', color: '#64748B', },
417
+ cameraFeedContainer: { flex: 1, position: 'relative', backgroundColor: '#000', },
418
+ 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
475
421
  },
422
+ refreshButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
476
423
  previewContainer: {
477
- width: '100%',
478
- backgroundColor: 'white',
479
- borderRadius: 12,
480
- paddingVertical: 24,
481
- paddingHorizontal: 20,
482
- shadowColor: '#000',
483
- shadowOffset: { width: 0, height: 4 },
484
- shadowOpacity: 0.1,
485
- shadowRadius: 12,
486
- elevation: 8,
424
+ width: '100%', backgroundColor: 'white', borderRadius: 12, paddingVertical: 24, paddingHorizontal: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8,
487
425
  ...(Platform.OS === 'web' ? { alignSelf: 'center', maxWidth: 600 } : { margin: 10, width: '95%' })
488
426
  },
489
- previewItemContainer: {
490
- flexGrow: 1,
491
- },
427
+ previewItemContainer: { flexGrow: 1, },
492
428
  title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 8, textAlign: 'center' },
493
429
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, lineHeight: 22 },
494
430
  sideContainer: { marginBottom: 24 },
495
431
  sideTitle: { fontSize: 25, fontWeight: 'bold', color: '#000', marginBottom: 12, textAlign: 'center' },
496
- imagePreviewWrapper: {
497
- width: '100%', height: 220, borderRadius: 12, padding: 1, overflow: 'hidden',
498
- shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0'
499
- },
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' },
500
433
  previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
501
434
  floatingErrorBanner: {
502
- position: 'absolute',
503
- bottom: 30, // Pushed to the bottom for professional feel
504
- left: 24,
505
- right: 24,
506
- backgroundColor: '#FEF2F2',
507
- borderWidth: 1,
508
- borderColor: '#FCA5A5',
509
- paddingVertical: 12,
510
- paddingHorizontal: 16,
511
- borderRadius: 12,
512
- alignItems: 'center',
513
- justifyContent: 'center',
514
- shadowColor: '#DC2626',
515
- shadowOffset: { width: 0, height: 4 },
516
- shadowOpacity: 0.1,
517
- shadowRadius: 8,
518
- elevation: 8,
519
- zIndex: 100
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
520
436
  },
521
437
  floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
522
438
  processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
@@ -527,7 +443,39 @@ const styles = StyleSheet.create({
527
443
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
528
444
  topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
529
445
  analyzingPillText: { color: 'white', fontSize: 14, fontWeight: 'bold' },
530
- refreshButton: { position: 'absolute', bottom: 100, alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.5)', padding: 10, borderRadius: 20, zIndex: 500 },
531
- refreshButtonText: { color: 'white', 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
480
  });
533
481
  //# sourceMappingURL=IDCardCapture.js.map