@sanctum-key/react-native-sdk 1.0.15 → 1.0.17

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanctum-key/react-native-sdk",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Sanctum Key React Native SDK",
5
5
  "main": "build/src/index.js",
6
6
  "types": "build/src/index.d.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAI5D,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;AAc5L,UAAU,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAAE;AAC3F,UAAU,kBAAkB;IAAG,SAAS,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAAE;AAExO,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAyftD,CAAC"}
1
+ {"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAI5D,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;AAc5L,UAAU,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAAE;AAC3F,UAAU,kBAAkB;IAAG,SAAS,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAAE;AAExO,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmftD,CAAC"}
@@ -157,8 +157,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
157
157
  setProcessingImagePath(capturePath);
158
158
  try {
159
159
  let imagePathForUpload = capturePath;
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
160
  const overlayBbox = cameraConfig.overlay.bbox;
163
161
  const uiCropBbox = {
164
162
  minX: overlayBbox.xMin / 100,
@@ -168,11 +166,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
168
166
  };
169
167
  try {
170
168
  // Apply the crop using the UI percentages and a tight 2% tolerance
169
+ // NOTE: If you integrated the Kotlin centered crop function earlier, replace this with cropToCenterScanArea
171
170
  imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
172
171
  }
173
172
  catch (e) {
174
173
  console.warn("Crop failed, falling back to original image", e);
175
- imagePathForUpload = capturePath; // Fallback to raw image if crop fails
174
+ imagePathForUpload = capturePath;
176
175
  }
177
176
  const base64 = await pathToBase64(imagePathForUpload);
178
177
  const newImages = {
@@ -260,8 +259,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
260
259
  await autoCapture(result.path, verifiedResult);
261
260
  }
262
261
  catch (error) {
263
- // 1. Keep the technical log for your debugging console
264
- console.error("Backend Verification Error:", error);
262
+ console.log("Backend Verification Error:", error);
265
263
  // 2. Define a map of technical keywords to user-friendly messages
266
264
  const rawMessage = (error?.message || '').toLowerCase();
267
265
  let userFriendlyMessage = 'Verification failed. Please try again.';
@@ -271,7 +269,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
271
269
  else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
272
270
  userFriendlyMessage = 'The document does not match your selected country.';
273
271
  }
274
- else if (rawMessage.includes('too far') || rawMessage.includes('card_not_fully_in_frame')) {
272
+ else if (rawMessage.includes('card_not_fully_in_frame')) {
273
+ // 🚨 REMOVED 'too far' CHECK FROM HERE
275
274
  userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
276
275
  }
277
276
  // 3. Set ONLY the friendly message to the UI state
@@ -294,48 +293,48 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
294
293
  }, [showCamera]);
295
294
  if (!countrySelectionData || !selectedDocumentType) {
296
295
  return (<View style={styles.root}>
297
- <View style={styles.previewContainer}>
298
- <Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
299
- <Text style={styles.description}>
300
- {state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
301
- </Text>
302
- </View>
303
- </View>);
296
+ <View style={styles.previewContainer}>
297
+ <Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
298
+ <Text style={styles.description}>
299
+ {state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
300
+ </Text>
301
+ </View>
302
+ </View>);
304
303
  }
305
304
  // --- CAMERA RENDER ---
306
305
  if (showCamera) {
307
306
  const isBusy = isProcessingCapture;
308
307
  if (isRebootingCamera) {
309
308
  return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
310
- <ActivityIndicator size="large" color="#2DBD60"/>
311
- <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
312
- </View>);
309
+ <ActivityIndicator size="large" color="#2DBD60"/>
310
+ <Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
311
+ </View>);
313
312
  }
314
313
  return (<View style={styles.root}>
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) : ''}
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>
326
- </View>
327
-
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>)}
314
+ <View style={[styles.cameraWrapper, { flex: 1 }]}>
315
+
316
+ <View style={styles.headerContainer}>
317
+ <Text style={styles.headerTitle}>
318
+ {selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
319
+ </Text>
320
+ <View style={styles.stepBadge}>
321
+ <Text style={styles.stepText}>
322
+ {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
323
+ </Text>
324
+ </View>
325
+ </View>
326
+
327
+ <View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
328
+
329
+ {/* WEB ONLY: Flip Camera Top Button */}
330
+ {Platform.OS === 'web' && (<View style={styles.webTopControls}>
331
+ <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
332
+ <Text style={styles.webFlipText}>🔄 Flip Lens</Text>
333
+ </TouchableOpacity>
334
+ </View>)}
336
335
 
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={{
336
+ <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
337
+ <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={{
339
338
  back: () => {
340
339
  if (currentSide === 'back') {
341
340
  setCurrentSide('front');
@@ -357,86 +356,83 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
357
356
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
358
357
  step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
359
358
  }}/>
360
- </>}/>
359
+ </>}/>
361
360
 
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>)}
361
+ {!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
362
+ <View style={styles.topAnalyzingPill}>
363
+ <ActivityIndicator size="small" color="white"/>
364
+ <Text style={styles.analyzingPillText}>
365
+ {state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
366
+ </Text>
367
+ </View>
368
+ </View>)}
370
369
 
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>)}
370
+ {isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
371
+ {processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
372
+ <View style={styles.processingOverlay}>
373
+ <ActivityIndicator size="large" color="#2DBD60"/>
374
+ <Text style={styles.processingText}>
375
+ {state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
376
+ </Text>
377
+ </View>
378
+ </View>)}
380
379
 
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 }}/>}
380
+ {/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
381
+ {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
382
+ {silentCaptureResult.error ? (<View style={styles.floatingErrorBanner}>
383
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
384
+ </View>) : <View style={{ height: 10 }}/>}
386
385
 
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}
386
+ <View style={styles.webActionButtonsRow}>
387
+ <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
388
+ <Text style={styles.webSecondaryButtonText}>Cancel</Text>
389
+ </TouchableOpacity>
390
+ <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
391
+ <Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
392
+ </TouchableOpacity>
393
+ </View>
394
+ </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
395
+ <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
396
+ <Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
397
+ </TouchableOpacity>
398
+ <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
399
+ <Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
400
+ </TouchableOpacity>
401
+ </View>) : null}
403
402
 
404
- {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
405
- <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
406
- </View>) : null}
403
+ {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
404
+ <Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
405
+ </View>) : null}
407
406
 
408
- </View>
409
- </View>
410
- </View>);
407
+ </View>
408
+ </View>
409
+ </View>);
411
410
  }
412
411
  // --- PREVIEW RENDER ---
413
412
  return (<View style={styles.root}>
414
- <View style={styles.previewContainer}>
415
- <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
416
- <View key={currentSide} style={styles.sideContainer}>
417
- <Text style={styles.sideTitle}>
418
- {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
419
- </Text>
420
- <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
421
- {getLocalizedText(component.instructions)}
422
- </Text>
423
-
424
- <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
425
- {silentCaptureResult?.error === 'TOO_FAR_AWAY' && (<View style={styles.warningBanner}>
426
- <Text style={styles.warningText}>
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."}
428
- </Text>
429
- </View>)}
430
-
431
- {/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
432
- <View style={styles.imagePreviewWrapper}>
433
- {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
434
- </View>
413
+ <View style={styles.previewContainer}>
414
+ <ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
415
+ <View key={currentSide} style={styles.sideContainer}>
416
+ <Text style={styles.sideTitle}>
417
+ {t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
418
+ </Text>
419
+ <Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
420
+ {getLocalizedText(component.instructions)}
421
+ </Text>
422
+
423
+ <View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
424
+
425
+ {/* 🚨 REMOVED THE ORANGE WARNING BANNER ENTIRELY */}
426
+
427
+ {/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
428
+ <View style={styles.imagePreviewWrapper}>
429
+ {capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
430
+ </View>
435
431
 
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/>)}
437
- {capturedImages[currentSide]?.dir && (<>
438
- <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
439
- <Button title={t('common.next')} onPress={() => {
432
+ {!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/>)}
433
+ {capturedImages[currentSide]?.dir && (<>
434
+ <Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
435
+ <Button title={t('common.next')} onPress={() => {
440
436
  if (!selectedDocumentType) {
441
437
  showAlert('Error', 'Document type not selected');
442
438
  return;
@@ -453,12 +449,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
453
449
  setProcessingImagePath(null);
454
450
  }
455
451
  }} variant="primary" size="large" fullWidth/>
456
- </>)}
457
- </View>
458
- </View>
459
- </ScrollView>
460
- </View>
461
- </View>);
452
+ </>)}
453
+ </View>
454
+ </View>
455
+ </ScrollView>
456
+ </View>
457
+ </View>);
462
458
  };
463
459
  const styles = StyleSheet.create({
464
460
  root: {
@@ -510,8 +506,8 @@ const styles = StyleSheet.create({
510
506
  imagePreviewWrapper: {
511
507
  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
508
  ...Platform.select({
513
- web: { aspectRatio: 1.59, height: 'auto' }, // 🚨 Perfect ID ratio on web
514
- default: { height: 220 } // 🚨 Strict original height on mobile
509
+ web: { aspectRatio: 1.59, height: 'auto' },
510
+ default: { height: 220 }
515
511
  })
516
512
  },
517
513
  previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
@@ -524,8 +520,6 @@ const styles = StyleSheet.create({
524
520
  floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
525
521
  processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
526
522
  processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
527
- warningBanner: { backgroundColor: '#FF9500', padding: 12, borderRadius: 8, width: '100%', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 4 },
528
- warningText: { color: 'white', fontWeight: 'bold', textAlign: 'center', fontSize: 16 },
529
523
  errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
530
524
  topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
531
525
  topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },