diff --git a/app/lib/cart.ts b/app/lib/cart.ts index fb76ccb..28f5f3c 100644 --- a/app/lib/cart.ts +++ b/app/lib/cart.ts @@ -150,32 +150,30 @@ export async function mergeServerCartIntoLocal(): Promise { // Cross-device sync for an *already* logged-in session — CartSync.tsx // only ever pushes local→server, and mergeServerCartIntoLocal() above only // ever runs at the moment of a fresh login/register, so a customer already -// signed in on device B never picked up an addition made on device A. This +// signed in on device B never picked up a change made on device A. This // covers that gap: called on mount and on window focus (CartSync.tsx), not // just at login. // -// Unlike mergeServerCartIntoLocal()'s additive `addToCart` (only safe +// Replaces the local cart outright with the server's — NOT an additive +// merge like mergeServerCartIntoLocal() above (that one is only safe // right after login, when the local cart can't yet overlap the server -// one — see that function's own comment), this must be idempotent: it can -// run repeatedly against a cart that's already in sync. For a line that -// exists on both sides, the server's qty wins outright (overwrite, not -// add) — otherwise calling this twice in a row would double the quantity -// every time. A local-only line (added on this device but not yet pushed -// by CartSync's 800ms debounce) is left untouched rather than dropped. +// one, see its own comment). An earlier version of this function only +// added/updated matching lines and never removed a local line the server +// no longer had, which meant a *removal* on device A never reached device +// B (confirmed live 2026-08-01 — adding synced, removing didn't). Server +// wins outright, including "server cart is now empty." +// +// Trade-off: a local addition made in the last few hundred ms — after +// this fetch went out but before CartSync's own 800ms debounce pushed it +// — could theoretically get overwritten by a pull that lands in between. +// Narrow enough (and self-healing on the next change/focus) to accept, +// consistent with every other "best-effort" sync path in this file. export async function pullServerCart(): Promise { try { const res = await fetch("/api/account/cart"); if (!res.ok) return; const data: { cart?: CartItem[] } = await res.json(); - const serverItems = data.cart ?? []; - if (serverItems.length === 0) return; - const items = readCart(); - for (const server of serverItems) { - const existing = items.find((i) => sameLine(i, server.id, server.variant)); - if (existing) existing.qty = server.qty; - else items.push(server.variant ? { id: server.id, qty: server.qty, variant: server.variant } : { id: server.id, qty: server.qty }); - } - writeCart(items); + writeCart(data.cart ?? []); } catch { // Best-effort — a failed pull just means this device doesn't see // another device's changes yet; nothing local is lost either way.