Sync the cart to an already-logged-in device, not just at login

Cross-device sync only ever worked one way (local→server on every
change via CartSync) plus a server→local merge at the exact moment of
login/register — a customer already signed in on a second device (no
fresh login action happening) never picked up changes made elsewhere.

Adds pullServerCart(): idempotent (server qty overwrites a matching
local line rather than adding to it, safe to call repeatedly, unlike
mergeServerCartIntoLocal()'s additive merge which only stays correct
right after a login clears the local cart's ambiguity). Triggered on
CartSync's mount and on window focus — covers "open the site while
already logged in" and "switch back to this tab after changing the
cart on another device," without a polling interval.
This commit is contained in:
Marco
2026-08-01 10:34:02 +00:00
parent 56c279b51b
commit ce3ac3e79a
2 changed files with 50 additions and 1 deletions
+35
View File
@@ -146,3 +146,38 @@ export async function mergeServerCartIntoLocal(): Promise<void> {
// as it was; nothing local is lost either way.
}
}
// 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
// covers that gap: called on mount and on window focus (CartSync.tsx), not
// just at login.
//
// Unlike mergeServerCartIntoLocal()'s additive `addToCart` (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.
export async function pullServerCart(): Promise<void> {
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);
} catch {
// Best-effort — a failed pull just means this device doesn't see
// another device's changes yet; nothing local is lost either way.
}
}