@sanctum-key/react-native-sdk 1.0.16 → 1.0.18

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