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:
@@ -2,21 +2,41 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
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() {
|
export default function CookieConsent() {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const acceptRef = useRef<HTMLButtonElement>(null);
|
const acceptRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
if (readConsent()) return; // already decided — stay hidden
|
||||||
const consent = localStorage.getItem("bb_cookie_consent");
|
const timer = setTimeout(() => setVisible(true), 1200);
|
||||||
if (!consent) {
|
return () => clearTimeout(timer);
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Focus the accept button when banner appears (accessibility)
|
// Focus the accept button when banner appears (accessibility)
|
||||||
@@ -27,16 +47,12 @@ export default function CookieConsent() {
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
try {
|
writeConsent("accepted");
|
||||||
localStorage.setItem("bb_cookie_consent", "accepted");
|
|
||||||
} catch {}
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDecline = () => {
|
const handleDecline = () => {
|
||||||
try {
|
writeConsent("declined");
|
||||||
localStorage.setItem("bb_cookie_consent", "declined");
|
|
||||||
} catch {}
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user