deckhearth/.convoys/scanner-mobile-checkout/brief-2-cart-model-session-storage.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

4 KiB

convoy brief_number depends_on recommended_model model_tier files cross_brief_commitments
scanner-mobile-checkout 2
composer-2.5-fast fast
lib/use-scanner-queue.js
lib/scanner-session.js
test/lib/scanner-session.test.js
test/lib/use-scanner-queue.test.js
brief description
4 Export `clearScannerCartStorage()` from `scanner-session.js`. Brief 4 calls it when the user confirms "Leave" on the back-guard modal (D1/D7). Queue hook exposes `hydrateFromStorage` on mount — Brief 4 does not reimplement hydration.

Brief 2: Cart model — stop auto-route + sessionStorage

Goal (1 sentence)

Reverse rebuild D3 auto-route: identifies enqueue locally only, auto-select new rows, and persist cart + selection in sessionStorage (D7).

Files in scope (do not edit anything else)

  • lib/use-scanner-queue.js
  • lib/scanner-session.js
  • test/lib/scanner-session.test.js
  • test/lib/use-scanner-queue.test.js (new)

Conventions to follow

  • No new API routes. Commit paths stay addScannedCardToOwned / addScannedCardToCollection from lib/scanner-route-api.js (already verified).
  • Do not use SCANNER_SESSION_STORAGE_KEY / localStorage for cart rows (UX anti-pattern). Add a separate key in scanner-session.js:
export const SCANNER_CART_STORAGE_KEY = 'deckhearth:scanner-cart';
  • Serialize selectedCards as number[] (Set is not JSON-safe).
  • Keep mergeScannedCardEntry unchanged — cart entry shape already includes processed: false and confidence from buildScannedCardPayload.
  • sessionDestination param may remain on the hook signature for backward compat but must not trigger network I/O in handleCardScanned.

Implementation shape (verified against use-scanner-queue.js)

Remove auto-route — delete the block after setScannedCards(nextQueue):

// DELETE lines ~148-168 (sessionDestination guard + routeScannedCardToDestination)

Replace handleCardScanned with enqueue-only:

const handleCardScanned = (cardData) => {
  setAutoRouteError(null);
  const { cardEntry, scannedCards: nextQueue } = mergeScannedCardEntry(
    scannedCards,
    cardData,
    scanDefaults
  );
  setScannedCards(nextQueue);
  setSelectedCards((prev) => new Set([...prev, cardEntry.id]));
  persistCart(nextQueue, new Set([...selectedCards, cardEntry.id]));
};

Add loadScannerCart() / saveScannerCart({ scannedCards, selectedCardIds }) / clearScannerCartStorage() in scanner-session.js.

Hydrate on mount in useScannerQueue via useState initializer + useEffect debounced save on [scannedCards, selectedCards] changes.

Checkout helpers (used by Brief 4 footer CTAs):

const commitSelectedToOwned = async () => handleBulkAction('owned');
const commitSelectedToCollection = async (collectionId) =>
  handleBulkAction('collection', collectionId);

Export these plus unprocessedCount derived helper: scannedCards.filter(c => !c.processed).length.

After successful handleBulkAction, remove committed rows from scannedCards entirely (not processed: true) per D1/D6 — cart holds only uncommitted items.

Acceptance criteria

  • handleCardScanned performs zero fetch calls (no routeScannedCardToDestination)
  • New enqueue auto-adds card id to selectedCards
  • Cart + selection survive sessionStorage round-trip within a tab (test/lib/scanner-session.test.js)
  • test/lib/use-scanner-queue.test.js: mock scanner-route-api and assert handleCardScanned never calls route helpers; assert handleBulkAction('owned') calls addScannedCardToOwned once per selected card
  • clearScannerCartStorage() clears persisted state
  • Existing test/lib/scanner-session.test.js cases still pass
  • No scope expansion

Rationale (≤3 sentences)

Cart semantics are pure client state and can land before any UI work. Separating sessionStorage from the existing localStorage destination prefs avoids D7/localStorage anti-pattern. Removing committed rows instead of flipping processed simplifies the checkout sheet list.