* 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>
5.5 KiB
| convoy | brief_number | depends_on | recommended_model | model_tier | files | cross_brief_commitments | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| scanner-mobile-checkout | 3 |
|
composer-2.5-fast | fast |
|
|
Brief 3: Camera chrome, facing mode, scan peek, gallery adapter
Goal (1 sentence)
Refactor ScannerCamera into full-bleed immersive glass chrome (top/bottom bars, flash, camera swap, Review N pill, scan peek, gallery) and add rear/front facingMode support in useCameraScanner.
Files in scope (do not edit anything else)
components/scanner/ScannerCamera.jscomponents/scanner/ScannerCountPill.jscomponents/scanner/ScannerScanPeek.js(new)components/scanner/ScannerToast.jslib/use-camera-scanner.jslib/use-scanner-identification.js(gallery adapter + pause contract only)
Conventions to follow
- Glass surfaces:
<GlassSurface tint="mid">per UX brief; migrate hardcodedrgba(0,0,0,0.55)inScannerCamerato tokens. ScannerToast: replaceICON_MAPUnicode glyphs with inline SVG check/error (design-direction checklist).ScannerCountPill: evolve to gradient Review {N} pill; keeparia-label={Review ${count} scanned cards}.- Flash:
flash.flashSupported && facingMode === 'environment'(UX spec). Hide on front camera; turning to front must calltoggleFlashoff if torch was on. - Peek: new
ScannerScanPeek.js— 3s auto-dismiss,pointer-events-nonewrapper, single child buttonpointer-events-auto, tap callsonOpenCheckout. - Copy: toast message
"{card.name} added"withrole="status". - Do not mount checkout sheet here — call
onOpenCheckoutonly. - Identification pipeline files (
scanner-card-identify.js, API routes) are out of scope.
useCameraScanner shape (verified)
Current startCamera hardcodes facingMode: 'environment' (line ~119). Extend:
export function useCameraScanner({ onError, onVerifyCard, verificationPausedRef, facingMode = 'environment' }) {
const [activeFacingMode, setActiveFacingMode] = useState(facingMode);
// startCamera uses activeFacingMode in getUserMedia constraints
const switchFacingMode = useCallback(() => {
setActiveFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment'));
}, []);
// useEffect: when activeFacingMode changes and stream exists, stopCamera + startCamera
return { /* existing */, facingMode: activeFacingMode, switchFacingMode };
}
verificationPausedRef early-return already exists at line ~91 in the tracking interval — no change needed.
Gallery adapter (verified — no existing file-picker path)
Add to useScannerIdentification return object:
const identifyFromGalleryFile = async (file) => {
if (!file || verificationPausedRef?.current) return;
const imageData = await readFileToImageData(file); // canvas draw; local helper in same file
const authHeaders = getScanAuthHeaders();
const result = await tryLayer1TextIdentify(imageData, authHeaders);
const outcome = resolveIdentifyOutcome(result);
const syntheticTracker = { id: `gallery-${Date.now()}`, status: 'verifying' };
await applyIdentifyOutcome(syntheticTracker, imageData, outcome);
};
Import tryLayer1TextIdentify, resolveIdentifyOutcome from ./scanner-card-identify.js (already exported — verified).
ScannerCamera props contract (for Brief 4)
export default function ScannerCamera({
queue,
camera,
identification,
onBack,
onOpenCheckout,
onGalleryIdentify, // (file) => identification.identifyFromGalleryFile(file)
latestPeekCard, // most recent unprocessed queue entry or null
cartCount,
isCheckoutOpen,
verificationPausedRef,
}) { /* ... */ }
Remove: deckMode, sessionDestination, onStopSession, phase-based stop button. Back button calls onBack (Brief 4 shows leave modal).
Layout: viewport min-h-[100dvh] max-md:rounded-none max-md:min-h-[100dvh]; top bar with back / "Scan Cards" / gallery file input; bottom bar flash | status | Review N.
Acceptance criteria
switchFacingModerestarts stream with toggledfacingMode; flash hidden onuserScannerScanPeekauto-dismisses ~3s; tap invokesonOpenCheckoutScannerToastuses SVG icons, not Unicode glyphsidentifyFromGalleryFilecalls existingtryLayer1TextIdentify— no API route changesScannerCountPillhidden whencount <= 0; shows gradient Review pill whencount > 0- No hex literals added in edited
.jsfiles - No scope expansion
Rationale (≤3 sentences)
Camera chrome files are disjoint from pages/scanner.js, so this brief can land after the cart model without file conflicts. Gallery needs a thin hook adapter because no file-picker path exists today (boot-the-brief finding). Brief 4 wires props and pause ref when the sheet opens.