Files
einfach-produktiv/app/components/CartSync.tsx
T
Marco ce3ac3e79a 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.
2026-08-01 10:34:02 +00:00

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;
}