ce3ac3e79a
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.
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
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`
|
|
// field). Renders nothing — mounted once in the root layout. Debounced
|
|
// (not fired on every keystroke-equivalent quantity bump) and silently a
|
|
// 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);
|
|
|
|
useEffect(() => {
|
|
// Skip the mount-time fire — this would otherwise POST on every page
|
|
// load even when nothing actually changed.
|
|
if (isFirstRender.current) {
|
|
isFirstRender.current = false;
|
|
return;
|
|
}
|
|
const timeout = setTimeout(() => {
|
|
fetch("/api/account/cart", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ cart }),
|
|
}).catch(() => {
|
|
// Best-effort — a failed sync just means the next cart change (or
|
|
// the next login-time merge) tries again.
|
|
});
|
|
}, 800);
|
|
return () => clearTimeout(timeout);
|
|
}, [cart]);
|
|
|
|
useEffect(() => {
|
|
pullServerCart();
|
|
window.addEventListener("focus", pullServerCart);
|
|
return () => window.removeEventListener("focus", pullServerCart);
|
|
}, []);
|
|
|
|
return null;
|
|
}
|