/** * 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)[ANALYTICS_CONSENT_META_KEY]; return raw === "accepted" || raw === "declined" ? raw : null; }