deckhearth/lib/scanner-session.js
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

131 lines
3.6 KiB
JavaScript

import { VOCAB } from './collection-vocabulary.js';
export const SCANNER_SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
export const SCANNER_CART_STORAGE_KEY = 'deckhearth:scanner-cart';
export const DEFAULT_SCANNER_DESTINATION = {
type: 'owned',
id: null,
label: VOCAB.MY_COLLECTION,
};
export const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false };
export function loadSavedScannerSession() {
if (typeof window === 'undefined') {
return {
destination: DEFAULT_SCANNER_DESTINATION,
gameFilter: 'All',
scanDefaults: DEFAULT_SCAN_DEFAULTS,
};
}
try {
const saved = JSON.parse(localStorage.getItem(SCANNER_SESSION_STORAGE_KEY));
return {
destination: saved?.destination || DEFAULT_SCANNER_DESTINATION,
gameFilter: saved?.gameFilter || 'All',
scanDefaults: saved?.scanDefaults || DEFAULT_SCAN_DEFAULTS,
};
} catch {
return {
destination: DEFAULT_SCANNER_DESTINATION,
gameFilter: 'All',
scanDefaults: DEFAULT_SCAN_DEFAULTS,
};
}
}
export function saveScannerSession({ destination, gameFilter, scanDefaults }) {
if (typeof window === 'undefined') return;
localStorage.setItem(
SCANNER_SESSION_STORAGE_KEY,
JSON.stringify({ destination, gameFilter, scanDefaults })
);
}
export function loadScannerCart() {
if (typeof window === 'undefined') {
return null;
}
try {
const saved = sessionStorage.getItem(SCANNER_CART_STORAGE_KEY);
if (!saved) return null;
const parsed = JSON.parse(saved);
if (!parsed || !Array.isArray(parsed.scannedCards)) return null;
return {
scannedCards: parsed.scannedCards,
selectedCardIds: Array.isArray(parsed.selectedCardIds) ? parsed.selectedCardIds : [],
};
} catch {
return null;
}
}
function toPersistedCartCard(card) {
if (!card || typeof card !== 'object') return card;
const { capturedImage, ocrText, ...safeCard } = card;
return safeCard;
}
export function saveScannerCart({ scannedCards, selectedCardIds }) {
if (typeof window === 'undefined') return;
sessionStorage.setItem(
SCANNER_CART_STORAGE_KEY,
JSON.stringify({
scannedCards: (scannedCards || []).map(toPersistedCartCard),
selectedCardIds,
})
);
}
export function clearScannerCartStorage() {
if (typeof window === 'undefined') return;
sessionStorage.removeItem(SCANNER_CART_STORAGE_KEY);
}
export function createScanCardId() {
return Date.now() + Math.random();
}
export function destinationActionKey(destination) {
if (!destination) return 'pending';
return destination.type === 'owned' ? 'owned' : destination.type;
}
export function mergeScannedCardEntry(existingCards, cardData, scanDefaults) {
const existingCardIndex = existingCards.findIndex(
(existing) =>
existing.name === cardData.name && existing.set === cardData.set && !existing.processed
);
if (existingCardIndex !== -1) {
const existing = existingCards[existingCardIndex];
const cardEntry = {
...existing,
quantity: (existing.quantity || 1) + 1,
scanImageUrl: cardData.scanImageUrl || existing.scanImageUrl,
timestamp: new Date().toISOString(),
};
const updated = [...existingCards];
updated[existingCardIndex] = cardEntry;
return { cardEntry, scannedCards: updated, merged: true };
}
const cardEntry = {
...cardData,
id: createScanCardId(),
name: cardData.name,
set: cardData.set,
quantity: 1,
condition: scanDefaults.condition,
isFoil: scanDefaults.isFoil,
timestamp: new Date().toISOString(),
processed: false,
};
return { cardEntry, scannedCards: [cardEntry, ...existingCards], merged: false };
}