From ce3ac3e79ab9a7443f28332eff3c692c77ca3041 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 1 Aug 2026 10:34:02 +0000 Subject: [PATCH] Sync the cart to an already-logged-in device, not just at login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/components/CartSync.tsx | 16 +++++++++++++++- app/lib/cart.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/components/CartSync.tsx b/app/components/CartSync.tsx index 55a16d2..caa51b8 100644 --- a/app/components/CartSync.tsx +++ b/app/components/CartSync.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef } from "react"; -import { useCart } from "../lib/cart"; +import { useCart, pullServerCart } from "../lib/cart"; // Mirrors the local cart to the server whenever it changes, so a logged-in // customer's cart follows them across devices (see Customers.ts's `cart` @@ -10,6 +10,14 @@ import { useCart } from "../lib/cart"; // no-op when logged out — POST /api/account/cart 401s in that case, which // this component doesn't need to distinguish from success; there's simply // nothing to keep in sync yet. +// +// Push (local→server) and pull (server→local) are both handled here, but +// deliberately asymmetric: push reacts to every local cart change (that's +// this device's news to share), pull only runs on mount and on window +// focus (see pullServerCart()'s own comment on why it's safe to call +// repeatedly) — no polling interval, since "another device changed my +// cart while this tab has been open and unfocused the whole time" is a +// rare enough case not to justify a persistent timer. export function CartSync() { const cart = useCart(); const isFirstRender = useRef(true); @@ -34,5 +42,11 @@ export function CartSync() { return () => clearTimeout(timeout); }, [cart]); + useEffect(() => { + pullServerCart(); + window.addEventListener("focus", pullServerCart); + return () => window.removeEventListener("focus", pullServerCart); + }, []); + return null; } diff --git a/app/lib/cart.ts b/app/lib/cart.ts index 18ffcce..fb76ccb 100644 --- a/app/lib/cart.ts +++ b/app/lib/cart.ts @@ -146,3 +146,38 @@ export async function mergeServerCartIntoLocal(): Promise { // 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 { + 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. + } +}