diff --git a/app/cart/components/FreeShippingBanner.tsx b/app/cart/components/FreeShippingBanner.tsx index a9a303e..2678f77 100644 --- a/app/cart/components/FreeShippingBanner.tsx +++ b/app/cart/components/FreeShippingBanner.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; -import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion, useInView } from "motion/react"; import { formatPrice } from "../../lib/format"; const SUCCESS_VISIBLE_MS = 2500; @@ -53,14 +53,29 @@ function FreeShippingBannerInner({ subtotal, threshold }: { subtotal: number; th } } + // Gates the hide-timer on the banner being CURRENTLY visible — deliberately + // not `{ once: true }`: since the banner sits at the very top of the cart + // page, it's already visible the instant the page loads (well before the + // user ever scrolls anywhere), so a lifetime "has this ever been seen" + // flag flips true immediately and defeats the whole point — reaching the + // threshold later while scrolled away would still start the timer right + // then, exactly the bug this was meant to fix. Continuous tracking + // instead: the effect below only runs the countdown while `isInView` is + // true, and its own cleanup cancels it the moment the banner scrolls back + // out of view — so it always takes a full uninterrupted 2.5s of the + // banner actually being on screen before it's allowed to hide, restarting + // if the user looks away mid-countdown and comes back. + const ref = useRef(null); + const isInView = useInView(ref); + // Effect's own cleanup (not a ref) cancels the pending hide-timer - // whenever phase changes away from "success" (e.g. dropping back below - // the threshold before the timer fires) or on unmount. + // whenever phase changes away from "success", the banner scrolls out of + // view, or on unmount. useEffect(() => { - if (phase !== "success") return; + if (phase !== "success" || !isInView) return; const t = setTimeout(() => setPhase("hidden"), SUCCESS_VISIBLE_MS); return () => clearTimeout(t); - }, [phase]); + }, [phase, isInView]); const remaining = Math.max(0, threshold - subtotal); const progressPct = Math.min(100, (subtotal / threshold) * 100); @@ -73,6 +88,7 @@ function FreeShippingBannerInner({ subtotal, threshold }: { subtotal: number; th {phase !== "hidden" && (