docs: add comprehensive Stripe billing guide
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
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.
This commit is contained in:
17
memento-note/lib/analytics/track.ts
Normal file
17
memento-note/lib/analytics/track.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Analytics choke-point — all product analytics must go through these helpers.
|
||||
* New third-party scripts in app/ must check hasAnalyticsConsent() before loading.
|
||||
*/
|
||||
import { hasAnalyticsConsent } from '@/lib/consent/cookie-consent'
|
||||
|
||||
export type AnalyticsEventProperties = Record<string, string | number | boolean | null | undefined>
|
||||
|
||||
export function trackClientEvent(_event: string, _properties?: AnalyticsEventProperties): void {
|
||||
if (!hasAnalyticsConsent()) return
|
||||
// Future: PostHog/Umami client capture (see saas-deployment-prep.md §D)
|
||||
}
|
||||
|
||||
export function trackServerEvent(_event: string, _properties?: AnalyticsEventProperties): void {
|
||||
if (!hasAnalyticsConsent()) return
|
||||
// Future: server-side analytics capture
|
||||
}
|
||||
112
memento-note/lib/consent/cookie-consent.ts
Normal file
112
memento-note/lib/consent/cookie-consent.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user