From ec89a88036afefe690650c90e57f05558af68e8f Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Thu, 14 May 2026 12:03:41 +0200 Subject: [PATCH 1/2] Finally fixes the stale checkout sidebar bug When customer was leaving checkout to storefront, changing cart and going back to checkout the sidebar wasn't re-rendered --- .../checkout/[id]/CheckoutPageContent.tsx | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx index 1fc9e4f7..d2236af1 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx @@ -24,6 +24,7 @@ import { import { PolicyConsent } from "@/components/policy/PolicyConsent"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useAuth } from "@/contexts/AuthContext"; +import { useCart } from "@/contexts/CartContext"; import { useCheckout } from "@/contexts/CheckoutContext"; import { trackAddPaymentInfo, @@ -58,6 +59,22 @@ const ExpressCheckoutButton = dynamic( { ssr: false }, ); +// Fingerprint of line-item state only. Used to detect when CartContext +// has a different set of line items than our local checkout cart — +// happens when Next's router cache restores a prior /checkout/[id] view +// after the user went back, added/removed/changed quantities, and +// returned. +// +// Intentionally excludes discount/total/tax/delivery fields: those +// change as a result of checkout-page mutations (apply discount, select +// shipping, etc.) that update local `cart` but not `contextCart`, and +// we don't want those legitimate divergences to trigger an overwrite. +// Line items don't change from checkout-page mutations. +function cartItemsFingerprint(cart: Cart | null): string { + if (!cart) return ""; + return (cart.items ?? []).map((i) => `${i.id}:${i.quantity}`).join(","); +} + interface CheckoutPageContentProps { cartId: string; urlCountry: string; @@ -74,6 +91,7 @@ function CheckoutPageContentInner({ const searchParams = useSearchParams(); const basePath = extractBasePath(pathname); const { setSummaryContent } = useCheckout(); + const { cart: contextCart } = useCart(); const t = useTranslations("checkout"); const tc = useTranslations("common"); const { user, loading: authLoading } = useAuth(); @@ -152,22 +170,9 @@ function CheckoutPageContentInner({ return result; }, []); - // Track cart key for sidebar updates — useLayoutEffect so the sidebar - // renders on the first paint (before the browser paints the empty slot) - const cartKey = cart - ? `${cart.id}-${cart.total}-${cart.total_quantity}-${cart.amount_due ?? ""}` - : null; - const prevOrderKeyRef = useRef(null); - + // useLayoutEffect so the sidebar renders on the first paint (before the + // browser paints the empty slot). Always re-publish when `cart` changes. useLayoutEffect(() => { - if ( - cartKey === prevOrderKeyRef.current && - prevOrderKeyRef.current !== null - ) { - return; - } - prevOrderKeyRef.current = cartKey; - if (cart) { setSummaryContent( { + if (!contextCart || contextCart.id !== cartId) return; + if (cartItemsFingerprint(contextCart) === cartItemsFingerprint(cart)) + return; + setCart(contextCart); + }, [contextCart, cartId, cart]); + // Handle email blur — persist email as the first backend call const handleEmailBlur = useCallback(async (email: string) => { const currentOrder = cartRef.current; From f6a5b5936f78a3213d124a4d35a910e7bf96606b Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Thu, 14 May 2026 12:13:12 +0200 Subject: [PATCH 2/2] Update CheckoutPageContent.tsx --- .../checkout/[id]/CheckoutPageContent.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx index d2236af1..f86b85a3 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/CheckoutPageContent.tsx @@ -267,7 +267,10 @@ function CheckoutPageContentInner({ // user added an item, went to checkout, went back, changed quantity, // and returned), our local `cart` keeps the old line items. CartContext // refreshes on every pathname change, so when its line-item fingerprint - // disagrees with ours, copy in the fresher cart. + // disagrees with ours, refetch the checkout cart so we get the new + // items plus recalculated totals — `contextCart` itself doesn't carry + // the checkout-side discount/shipping calculations, so we use it only + // as a staleness signal, not as the source of truth. // // We compare line items only, not totals — checkout-page mutations // (apply discount, select shipping, etc.) change totals locally but @@ -281,8 +284,24 @@ function CheckoutPageContentInner({ if (!contextCart || contextCart.id !== cartId) return; if (cartItemsFingerprint(contextCart) === cartItemsFingerprint(cart)) return; - setCart(contextCart); - }, [contextCart, cartId, cart]); + let cancelled = false; + getCheckoutOrder(cartId) + .then((fresh) => { + if (cancelled || !fresh) return; + if (fresh.current_step === "complete") { + routerRef.current.push(`${basePath}/order-placed/${cartId}`); + return; + } + setCart(fresh); + }) + .catch(() => { + // Best-effort refresh — keep the current cart on failure rather + // than wiping checkout state. + }); + return () => { + cancelled = true; + }; + }, [contextCart, cartId, cart, basePath]); // Handle email blur — persist email as the first backend call const handleEmailBlur = useCallback(async (email: string) => {