--- convoy: scanner-mobile-checkout brief_number: 4 depends_on: [1, 2, 3] recommended_model: composer-2.5-fast model_tier: fast files: - pages/scanner.js - pages/login.js - components/scanner/ScannerCheckoutSheet.js - components/scanner/ScannerReview.js - components/scanner/ReviewCardItem.js - test/components/ScannerCheckoutSheet.test.js cross_brief_commitments: - brief: 1 description: | Passes `chrome="immersive"` to `` on authenticated `/scanner` (mobile immersive; desktop uses default chrome per D3). - brief: 2 description: | Calls `clearScannerCartStorage()` on confirmed leave. Uses `commitSelectedToOwned` / `commitSelectedToCollection` from queue hook. Does not reimplement sessionStorage persistence. - brief: 3 description: | Wires `ScannerCamera` props (`onBack`, `onOpenCheckout`, `latestPeekCard`, `onGalleryIdentify`, `cartCount`, `isCheckoutOpen`, `verificationPausedRef`). Sets `verificationPausedRef.current = true` when checkout sheet or List picker modal is open (mobile); desktop side panel does not pause (UX spec). deletes: [] --- # Brief 4: Page orchestration + checkout sheet ## Goal (1 sentence) Collapse `/scanner` to scanning-first immersive flow, wire checkout sheet (mobile) / side panel (desktop), auth return URL (D2), and commit CTAs using vocab constants. ## Files in scope (do not edit anything else) - `pages/scanner.js` - `pages/login.js` - `components/scanner/ScannerCheckoutSheet.js` (new) - `components/scanner/ScannerReview.js` (desktop `md+` side panel only) - `components/scanner/ReviewCardItem.js` - `test/components/ScannerCheckoutSheet.test.js` (new) ## Conventions to follow - **Vocab:** `import { VOCAB } from '../lib/collection-vocabulary.js'` — primary `VOCAB.ADD_TO_MY_COLLECTION`, secondary `VOCAB.ADD_TO_LIST`. Never "Save to binder" / "Mark Owned". - **Sheet shell:** copy `ScannerDisambiguation.js` pattern (`fixed inset-0`, `glass-panel-strong`, `useFocusTrap`, Escape dismiss, `z-40` — disambiguation stays `z-50`). - **Modal primitive:** back-guard and List picker use `` from `components/ui/Modal.js` (not `window.confirm`). - **Selection:** checkboxes always visible; all unprocessed IDs selected on first sheet open; new enqueue while open auto-selects (Brief 2 + local effect). - **Pause:** `verificationPausedRef.current = isCheckoutOpen || isListPickerOpen || identification.disambiguation` on mobile only. - **Auth:** `getUserFromRequest` not needed (page is client-only). Login redirect: ```js // pages/scanner.js — replace router.push('/login') router.push(`/login?returnUrl=${encodeURIComponent('/scanner')}`); // pages/login.js — after successful login, before role redirect: const returnUrl = typeof router.query.returnUrl === 'string' ? router.query.returnUrl : null; if (returnUrl && returnUrl.startsWith('/')) { router.push(returnUrl); return; } ``` - **D1:** after commit, stay on camera; empty cart closes sheet and resumes scanning. - **D4:** no deck destination in checkout UI. - **D6:** partial commit keeps sheet open with remaining rows. ## `pages/scanner.js` shape (verified — replace phase machine) Remove `phase` state (`setup` / `scanning` / `review`). Default: camera always mounted when authenticated. ```js export default function Scanner() { const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); const [isListPickerOpen, setIsListPickerOpen] = useState(false); const [showLeaveModal, setShowLeaveModal] = useState(false); const verificationPausedRef = useRef(false); // useScannerSession: keep scanDefaults only; drop Setup UI (D3) const queue = useScannerQueue({ user, scanDefaults, deckMode: null }); // ... return (
unprocessedCount > 0 ? setShowLeaveModal(true) : router.back()} onOpenCheckout={() => setIsCheckoutOpen(true)} onGalleryIdentify={(file) => identification.identifyFromGalleryFile(file)} latestPeekCard={queue.scannedCards[0] ?? null} cartCount={unprocessedCount} isCheckoutOpen={isCheckoutOpen} verificationPausedRef={verificationPausedRef} /> {/* Mobile sheet */}
{isCheckoutOpen && ( setIsCheckoutOpen(false)} onOpenListPicker={() => setIsListPickerOpen(true)} /> )}
{/* Desktop side panel */}
); } ``` Sync `verificationPausedRef` in `useEffect` when `isCheckoutOpen || isListPickerOpen || identification.disambiguation` (mobile: checkout open; desktop: only list picker + disambiguation per UX). ## `ScannerCheckoutSheet` responsibilities - Header: `{N} cards scanned` + low-confidence subline when any row `confidence < 70` - Rows: `ReviewCardItem` with checkbox, confidence ring, qty stepper, overflow menu (remove / condition / foil) - Footer: primary `VOCAB.ADD_TO_MY_COLLECTION` with `(N)` when subset selected; secondary `VOCAB.ADD_TO_LIST` - Commit error: inline banner in footer, rows stay selected - `role="dialog"` `aria-modal="true"` `aria-labelledby` - On last item removed → `onClose()` + resume camera ## `ReviewCardItem` changes - Add optional `selected`, `onToggleSelect`, `showCheckbox`, `confidence` ring props - Remove inline destination picker from row (destinations in sheet footer only) - `aria-label` on checkbox: `Select ${card.name}`; confidence in `aria-label` when `< 85` ## `ScannerReview` changes - Support `variant="side-panel"` for desktop persistent panel (same row/footer semantics as sheet, no scrim) - Remove full-page empty state / "New Session" / deck progress from default path ## Acceptance criteria - [ ] Opening `/scanner` authenticated shows camera immediately — no Setup phase (D3) - [ ] Unauthenticated redirect includes `returnUrl=/scanner`; login honors it (D2) - [ ] Mobile checkout sheet opens from Review N and peek; pauses identification under sheet - [ ] `commitSelectedToOwned` / List commit work; committed rows leave cart; camera resumes when empty (D1/D6) - [ ] Back with unprocessed cart shows `` leave confirm; confirm calls `clearScannerCartStorage()` + `router.back()` - [ ] Desktop `md+` shows side panel with same CTA semantics; Layout sidebar visible - [ ] `test/components/ScannerCheckoutSheet.test.js`: renders header count, disabled primary when nothing selected, vocab labels present - [ ] No scope expansion ## Rationale (≤3 sentences) Integration brief owns the sole `pages/scanner.js` writer after Layout and camera contracts exist. Login `returnUrl` is a two-line change required by D2 and verified absent today. Checkout sheet and desktop panel share row components but split shells to stay under 400 LOC.