* 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>
4 KiB
| convoy | brief_number | depends_on | recommended_model | model_tier | files | cross_brief_commitments | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| scanner-mobile-checkout | 2 | composer-2.5-fast | fast |
|
|
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.jslib/scanner-session.jstest/lib/scanner-session.test.jstest/lib/use-scanner-queue.test.js(new)
Conventions to follow
- No new API routes. Commit paths stay
addScannedCardToOwned/addScannedCardToCollectionfromlib/scanner-route-api.js(already verified). - Do not use
SCANNER_SESSION_STORAGE_KEY/localStoragefor cart rows (UX anti-pattern). Add a separate key inscanner-session.js:
export const SCANNER_CART_STORAGE_KEY = 'deckhearth:scanner-cart';
- Serialize
selectedCardsasnumber[](Set is not JSON-safe). - Keep
mergeScannedCardEntryunchanged — cart entry shape already includesprocessed: falseandconfidencefrombuildScannedCardPayload. sessionDestinationparam may remain on the hook signature for backward compat but must not trigger network I/O inhandleCardScanned.
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
handleCardScannedperforms zerofetchcalls (norouteScannedCardToDestination)- New enqueue auto-adds card
idtoselectedCards - Cart + selection survive
sessionStorageround-trip within a tab (test/lib/scanner-session.test.js) test/lib/use-scanner-queue.test.js: mockscanner-route-apiand asserthandleCardScannednever calls route helpers; asserthandleBulkAction('owned')callsaddScannedCardToOwnedonce per selected cardclearScannerCartStorage()clears persisted state- Existing
test/lib/scanner-session.test.jscases 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.