lib/cookie-consent.ts
Rama main · réplica de lectura del repositorio original.
/**
* Consent to analytical cookies (MIN-77 / legal pages).
*
* The choice lives in the browser's localStorage, never on the server side: it's
* a device preference, not an account data. As long as no choice has
* been made, the value is `null` and NO analytical cookies should be set.
*
* This contract is honored by `components/posthog-init.tsx` (MIN-78): it reads
* `readConsent()` before capture and listens for local and cross-tab changes.
* Before acceptance, measurement uses page memory or optional server hashing,
* without persisting an analytics identifier. A refusal blocks all browser
* capture, even when the SDK could otherwise continue cookieless measurement.
* The choice is reread after lazy loading so a click during download is honored.
*/
export const COOKIE_CONSENT_KEY = "cookie_consent";
export const CONSENT_CHANGED_EVENT = "minddy:cookie-consent-changed";
export type CookieConsent = "accepted" | "declined";
let volatileConsent: CookieConsent | undefined;
/** The saved choice, or null if the user has not yet decided. */
export function readConsent(): CookieConsent | null {
if (typeof window === "undefined") return null;
if (volatileConsent !== undefined) return volatileConsent;
try {
const raw = window.localStorage.getItem(COOKIE_CONSENT_KEY);
return raw === "accepted" || raw === "declined" ? raw : null;
} catch {
// localStorage unavailable (strict private browsing) → no consent.
return null;
}
}
/** Saves the choice and notifies the listeners (analytics) in the tab. */
export function writeConsent(consent: CookieConsent): void {
try {
window.localStorage.setItem(COOKIE_CONSENT_KEY, consent);
volatileConsent = undefined;
} catch {
// Honor the choice for this page even when durable preference storage fails.
volatileConsent = consent;
}
window.dispatchEvent(new Event(CONSENT_CHANGED_EVENT));
}
/* ─── The copy that SURVIVES the device ─────────────────────────────── */
/**
* The same choice, in the account (MIN-293).
*
* The localStorage alone was enough as long as the question arose in a
* browser, which keeps its storage forever. **In the desktop app,
* no**: the question arises in blocking mode at launch, and the slightest
* new profile — a reinstallation, a dev shell, a session restarted from
* zero — rested it. A question asked "once" which comes back at each
* launch is no longer a question, it is the banner that was replaced.
*
* Hence the copy in `user_metadata`, written next to the local storage and never
* in its place. The sharing of roles counts:
*
* - **the localStorage remains the source of truth of the MEASUREMENT.** It is he that
* `posthog-init` reads, synchronously, before any session; the account
* does not yet exist at that moment, and waiting for a session to decide to
* setting a cookie would be taking it in reverse;
* - **the account is the source of truth of the QUESTION.** A device without a local choice
* but whose account has one adopts that of the account, without ask again.
*
* What this doesn't change: a refusal remains a refusal everywhere, and the agreement of a
* device does not pose anything to another as long as you do not connect to it.
*/
export const ANALYTICS_CONSENT_META_KEY = "analytics_consent";
/** The choice made by the account, or `null` if it does not have one. */
export function resolveAnalyticsConsent(meta: unknown): CookieConsent | null {
if (!meta || typeof meta !== "object") return null;
const raw = (meta as Record<string, unknown>)[ANALYTICS_CONSENT_META_KEY];
return raw === "accepted" || raw === "declined" ? raw : null;
}