deckhearth/.convoys/scanner-mobile-checkout/brief-3-camera-chrome-facing-peek.md
varutasu 73424aae59
Mobile scanner checkout: scan first, commit later (#157)
* 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>
2026-08-14 20:20:43 -05:00

5.5 KiB

convoy brief_number depends_on recommended_model model_tier files cross_brief_commitments
scanner-mobile-checkout 3
2
composer-2.5-fast fast
components/scanner/ScannerCamera.js
components/scanner/ScannerCountPill.js
components/scanner/ScannerScanPeek.js
components/scanner/ScannerToast.js
lib/use-camera-scanner.js
lib/use-scanner-identification.js
brief description
4 `ScannerCamera` accepts orchestration props wired by Brief 4: `onBack`, `onOpenCheckout`, `onGalleryIdentify`, `latestPeekCard`, `cartCount`, `isCheckoutOpen`, `verificationPausedRef`. Defaults/no-ops keep the component renderable before Brief 4 lands. Brief 4 owns sheet open state and back-guard modal.
brief description
4 `useScannerIdentification` exports `identifyFromGalleryFile(file)` — thin adapter only; does not modify `pages/api/scan/identify.js` or `lib/scanner-card-identify.js` server contract.

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.js
  • components/scanner/ScannerCountPill.js
  • components/scanner/ScannerScanPeek.js (new)
  • components/scanner/ScannerToast.js
  • lib/use-camera-scanner.js
  • lib/use-scanner-identification.js (gallery adapter + pause contract only)

Conventions to follow

  • Glass surfaces: <GlassSurface tint="mid"> per UX brief; migrate hardcoded rgba(0,0,0,0.55) in ScannerCamera to tokens.
  • ScannerToast: replace ICON_MAP Unicode glyphs with inline SVG check/error (design-direction checklist).
  • ScannerCountPill: evolve to gradient Review {N} pill; keep aria-label={Review ${count} scanned cards}.
  • Flash: flash.flashSupported && facingMode === 'environment' (UX spec). Hide on front camera; turning to front must call toggleFlash off if torch was on.
  • Peek: new ScannerScanPeek.js — 3s auto-dismiss, pointer-events-none wrapper, single child button pointer-events-auto, tap calls onOpenCheckout.
  • Copy: toast message "{card.name} added" with role="status".
  • Do not mount checkout sheet here — call onOpenCheckout only.
  • 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.

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

  • switchFacingMode restarts stream with toggled facingMode; flash hidden on user
  • ScannerScanPeek auto-dismisses ~3s; tap invokes onOpenCheckout
  • ScannerToast uses SVG icons, not Unicode glyphs
  • identifyFromGalleryFile calls existing tryLayer1TextIdentify — no API route changes
  • ScannerCountPill hidden when count <= 0; shows gradient Review pill when count > 0
  • No hex literals added in edited .js files
  • 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.