* Start scanner-mobile-checkout convoy for the cart-then-commit phone flow. Co-authored-by: Cursor <cursoragent@cursor.com> * Ship a cart-then-commit mobile scanner so phone sessions stay on the camera. Scan matches enqueue locally instead of auto-writing ownership, checkout happens in a sheet, and audit fixes cover stale commit detection, returnUrl open redirects, nested Escape, and ember detection chrome. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
3 KiB
JavaScript
97 lines
3 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import GlassSurface from '../ui/GlassSurface.js';
|
|
|
|
const PEEK_DURATION_MS = 3000;
|
|
|
|
function formatConfidence(confidence) {
|
|
if (confidence == null || Number.isNaN(confidence)) return null;
|
|
const pct = confidence <= 1 ? Math.round(confidence * 100) : Math.round(confidence);
|
|
return `${pct}%`;
|
|
}
|
|
|
|
export default function ScannerScanPeek({ card, onOpenCheckout = () => {} }) {
|
|
const [visible, setVisible] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const timerId = window.setTimeout(() => {
|
|
setVisible(false);
|
|
}, PEEK_DURATION_MS);
|
|
return () => window.clearTimeout(timerId);
|
|
}, []);
|
|
|
|
if (!card || !visible) return null;
|
|
|
|
const cardName = card.name || card.card?.name || 'Card';
|
|
const thumbnail =
|
|
card.image_url ||
|
|
card.scanImageUrl ||
|
|
card.card?.image_url ||
|
|
null;
|
|
const confidenceLabel = formatConfidence(card.confidence);
|
|
|
|
return (
|
|
<div
|
|
className="absolute left-4 right-4 z-20 pointer-events-none"
|
|
style={{
|
|
bottom: 'calc(5.5rem + env(safe-area-inset-bottom, 0px))',
|
|
opacity: visible ? 1 : 0,
|
|
transform: `translateY(${visible ? '0' : '12px'})`,
|
|
transition: 'opacity 150ms ease, transform 150ms ease',
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onOpenCheckout}
|
|
className="w-full pointer-events-auto text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-2xl"
|
|
style={{
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
'--tw-ring-offset-color': 'transparent',
|
|
}}
|
|
aria-label={`Open cart to review ${cardName}`}
|
|
>
|
|
<GlassSurface
|
|
tint="mid"
|
|
rim="subtle"
|
|
blur="mid"
|
|
className="flex items-center gap-3 rounded-2xl px-3 py-2.5"
|
|
>
|
|
{thumbnail ? (
|
|
// eslint-disable-next-line @next/next/no-img-element -- ephemeral scan thumbnail blob/url
|
|
<img
|
|
src={thumbnail}
|
|
alt=""
|
|
className="w-12 h-[4.25rem] object-cover rounded-md flex-shrink-0"
|
|
style={{ border: '1px solid var(--border)' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="w-12 h-[4.25rem] rounded-md flex-shrink-0 flex items-center justify-center text-xs font-medium"
|
|
style={{
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
color: 'var(--text-secondary)',
|
|
border: '1px solid var(--border)',
|
|
}}
|
|
aria-hidden="true"
|
|
>
|
|
?
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<p
|
|
className="text-sm font-semibold truncate"
|
|
style={{ color: 'var(--text-primary)' }}
|
|
>
|
|
{cardName}
|
|
</p>
|
|
{confidenceLabel && (
|
|
<p className="text-xs mt-0.5" style={{ color: 'var(--text-secondary)' }}>
|
|
{confidenceLabel} match
|
|
</p>
|
|
)}
|
|
</div>
|
|
</GlassSurface>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|