From b21ac70e0cbf02c1ca9859a7bac32e0bf58e273d Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Tue, 19 May 2026 22:53:19 +0000 Subject: [PATCH] ui(consent): persist cookie-banner choice in a cookie (365-day) + localStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the banner only wrote bb_cookie_consent to localStorage. Many browsers wipe localStorage in strict-tracking-prevention modes, on "clear site data on close", and in some private-browsing-adjacent profiles — so the banner re-appeared on every visit even after the user clicked Accept or Decline. Now writes to BOTH a `bb_cookie_consent` cookie (path=/, max-age=365d, SameSite=Lax) and localStorage. Reads from cookie first, falls back to localStorage for backward compatibility with prior visitors. Banner stays hidden for 365 days after a decision is made. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/CookieConsent.tsx | 48 +++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/components/CookieConsent.tsx b/src/components/CookieConsent.tsx index 312d870..abd2b59 100644 --- a/src/components/CookieConsent.tsx +++ b/src/components/CookieConsent.tsx @@ -2,21 +2,41 @@ import React, { useState, useEffect, useRef } from "react"; import Link from "next/link"; +const CONSENT_KEY = "bb_cookie_consent"; +const CONSENT_DAYS = 365; + +function readConsent(): string | null { + // Cookie first — survives most "clear localStorage on close" settings. + if (typeof document !== "undefined") { + const m = document.cookie.match(/(?:^|;\s*)bb_cookie_consent=([^;]+)/); + if (m) return decodeURIComponent(m[1]); + } + try { + return localStorage.getItem(CONSENT_KEY); + } catch { + return null; + } +} + +function writeConsent(value: "accepted" | "declined") { + // Write to BOTH so we survive browsers that wipe one but not the other. + try { + const maxAge = CONSENT_DAYS * 24 * 60 * 60; + document.cookie = `${CONSENT_KEY}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`; + } catch {} + try { + localStorage.setItem(CONSENT_KEY, value); + } catch {} +} + export default function CookieConsent() { const [visible, setVisible] = useState(false); const acceptRef = useRef(null); useEffect(() => { - try { - const consent = localStorage.getItem("bb_cookie_consent"); - if (!consent) { - // Delay slightly so it doesn't flash on first paint - const timer = setTimeout(() => setVisible(true), 1200); - return () => clearTimeout(timer); - } - } catch { - // localStorage unavailable (SSR or private mode) - } + if (readConsent()) return; // already decided — stay hidden + const timer = setTimeout(() => setVisible(true), 1200); + return () => clearTimeout(timer); }, []); // Focus the accept button when banner appears (accessibility) @@ -27,16 +47,12 @@ export default function CookieConsent() { }, [visible]); const handleAccept = () => { - try { - localStorage.setItem("bb_cookie_consent", "accepted"); - } catch {} + writeConsent("accepted"); setVisible(false); }; const handleDecline = () => { - try { - localStorage.setItem("bb_cookie_consent", "declined"); - } catch {} + writeConsent("declined"); setVisible(false); };