* 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>
131 lines
3.6 KiB
JavaScript
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 };
|
|
}
|