All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
Covers architecture, configuration steps, user flows, API routes, webhooks, pricing, testing with Stripe CLI, production checklist, and troubleshooting.
113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
export const CONSENT_VERSION = 1 as const
|
|
export const CONSENT_STORAGE_KEY = 'memento-consent-v1'
|
|
export const CONSENT_COOKIE_NAME = 'memento-cookie-consent'
|
|
const CONSENT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
|
|
|
export type ConsentRecord = {
|
|
version: typeof CONSENT_VERSION
|
|
necessary: true
|
|
analytics: boolean
|
|
marketing: boolean
|
|
updatedAt: string
|
|
}
|
|
|
|
export const CONSENT_CHANGE_EVENT = 'memento-consent-change'
|
|
export const OPEN_COOKIE_PREFERENCES_EVENT = 'memento-open-cookie-preferences'
|
|
|
|
function isBrowser(): boolean {
|
|
return typeof window !== 'undefined'
|
|
}
|
|
|
|
function parseRecord(raw: unknown): ConsentRecord | null {
|
|
if (!raw || typeof raw !== 'object') return null
|
|
const o = raw as Partial<ConsentRecord>
|
|
if (o.version !== CONSENT_VERSION) return null
|
|
if (o.necessary !== true) return null
|
|
if (typeof o.analytics !== 'boolean' || typeof o.marketing !== 'boolean') return null
|
|
if (typeof o.updatedAt !== 'string') return null
|
|
return {
|
|
version: CONSENT_VERSION,
|
|
necessary: true,
|
|
analytics: o.analytics,
|
|
marketing: o.marketing,
|
|
updatedAt: o.updatedAt,
|
|
}
|
|
}
|
|
|
|
function readCookie(): ConsentRecord | null {
|
|
if (!isBrowser()) return null
|
|
const match = document.cookie
|
|
.split(';')
|
|
.map((s) => s.trim())
|
|
.find((s) => s.startsWith(`${CONSENT_COOKIE_NAME}=`))
|
|
if (!match) return null
|
|
try {
|
|
const encoded = match.slice(CONSENT_COOKIE_NAME.length + 1)
|
|
const json = decodeURIComponent(atob(encoded))
|
|
return parseRecord(JSON.parse(json))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeCookie(record: ConsentRecord): void {
|
|
if (!isBrowser()) return
|
|
const encoded = btoa(encodeURIComponent(JSON.stringify(record)))
|
|
document.cookie = `${CONSENT_COOKIE_NAME}=${encoded};path=/;max-age=${CONSENT_COOKIE_MAX_AGE};samesite=lax`
|
|
}
|
|
|
|
export function getConsent(): ConsentRecord | null {
|
|
if (!isBrowser()) return null
|
|
try {
|
|
const fromStorage = localStorage.getItem(CONSENT_STORAGE_KEY)
|
|
if (fromStorage) {
|
|
const parsed = parseRecord(JSON.parse(fromStorage))
|
|
if (parsed) return parsed
|
|
}
|
|
} catch {
|
|
// ignore corrupt storage
|
|
}
|
|
return readCookie()
|
|
}
|
|
|
|
export function hasConsentChoice(): boolean {
|
|
return getConsent() !== null
|
|
}
|
|
|
|
export function hasAnalyticsConsent(): boolean {
|
|
return getConsent()?.analytics === true
|
|
}
|
|
|
|
export function hasMarketingConsent(): boolean {
|
|
return getConsent()?.marketing === true
|
|
}
|
|
|
|
export function setConsent(partial: Pick<ConsentRecord, 'analytics' | 'marketing'>): ConsentRecord {
|
|
const record: ConsentRecord = {
|
|
version: CONSENT_VERSION,
|
|
necessary: true,
|
|
analytics: partial.analytics,
|
|
marketing: partial.marketing,
|
|
updatedAt: new Date().toISOString(),
|
|
}
|
|
if (isBrowser()) {
|
|
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(record))
|
|
writeCookie(record)
|
|
window.dispatchEvent(new CustomEvent(CONSENT_CHANGE_EVENT, { detail: record }))
|
|
}
|
|
return record
|
|
}
|
|
|
|
export function acceptEssentialsOnly(): ConsentRecord {
|
|
return setConsent({ analytics: false, marketing: false })
|
|
}
|
|
|
|
export function acceptAllOptional(): ConsentRecord {
|
|
return setConsent({ analytics: true, marketing: false })
|
|
}
|
|
|
|
export function openCookiePreferences(): void {
|
|
if (!isBrowser()) return
|
|
window.dispatchEvent(new CustomEvent(OPEN_COOKIE_PREFERENCES_EVENT))
|
|
}
|