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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user