ui(consent): persist cookie-banner choice in a cookie (365-day) + localStorage

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) <noreply@anthropic.com>
This commit is contained in:
Ryan Salazar
2026-05-19 22:53:19 +00:00
parent 28a910a7f8
commit b21ac70e0c

View File

@@ -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<HTMLButtonElement>(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);
};