Story 3.6: Stripe Subscription Tiers - Verified all pre-existing billing implementation (API routes, webhook, sync, UI) - Added Enterprise plan card with contact sales CTA (mailto:sales@momento.app) - Fixed lib/stripe.ts build error (lazy getStripe() + placeholder default) - Added enterpriseFeature1-5 i18n keys to all 15 locales - 22/22 unit tests pass, build succeeds - Story status: ready-for-dev → review
12 KiB
Story 4.1: GDPR Cookie Consent Management
Status: ready-for-dev
Story
As a visitor, I want to granularly accept or reject analytics and tracking cookies, so that my ePrivacy rights are respected.
Epic: Epic 4 — Enterprise Compliance & Privacy (B2B Requirements)
FR coverage: NFR-GDPR1 (granular accept/reject of analytics and tracking cookies; strictly necessary cookies remain enforced)
Out of scope for this story: Story 4.4 (explicit AI processing consent modal), Story 4.2 (account deletion), PostHog/Umami full integration (gate only; no analytics vendor required to ship 4.1)
Acceptance Criteria
- [AC1] First-visit banner (NFR-GDPR1): On any route (including
/login,/register, marketing/home), if no valid consent record exists, a fixed bottom banner appears. It cannot be dismissed without an explicit choice: Accept essentials only, Reject non-essential, or Manage preferences. - [AC2] Granular toggles: In “Manage preferences”, the user can independently toggle Analytics (and optionally Marketing if reserved for future ads pixels — default off, disabled or hidden until used). Strictly necessary cookies are listed as always on, not toggleable, with short explanations.
- [AC3] Strictly necessary enforced: Auth/session cookies (NextAuth), security, and functional preference cookies (
user-language, theme-related storage) continue to work without analytics consent. No analytics/tracking script or non-essential third-party beacon loads before consent. - [AC4] Persistence: Consent is stored client-side (
localStorage+ optionalmemento-cookie-consentcookie for SSR hints) with schema version, timestamp, and category flags. Re-opening preferences from Settings updates the same record. - [AC5] Authenticated sync: When a logged-in user accepts analytics, persist
UserAISettings.anonymousAnalytics = truevia existingupdateAISettings. On reject, setfalse. Guests rely on local storage only. - [AC6] Analytics gate: Introduce a single choke-point
lib/analytics/track.ts(orlib/consent/analytics.ts) where all future product analytics calls no-op unlesshasAnalyticsConsent(). Do not add PostHog/Umami packages in this story; wireErrorReporterand any future trackers through the gate policy documented below. - [AC7] Settings re-entry: Settings → General (or About) includes a “Cookie preferences” control that reopens the same manage UI without clearing prior choices.
- [AC8] i18n & design: All user-visible strings via
memento-note/locales/*.json(15 files; FR + EN as content reference). Banner uses existing design tokens (--ink,--concrete,--border,--memento-paper, uppercase micro-labels) — no new blue/legacy theme colors; matchesdocs/ux-design-specification.md§Emplacement Légal. - [AC9] Regression: Language cookie/localStorage flow in
general-settings-client.tsx, NextAuth login, theme/direction initializers, sidebar, billing (3.6), BYOK (3.5), and AI features work unchanged when user chooses essentials-only. - [AC10] No destructive DB: No
prisma migrate reset. If schema change is needed for consent audit log, prefer no migration in 4.1 (client +anonymousAnalyticsonly).
Tasks / Subtasks
- Task 1: Consent model & utilities (AC: #2, #4, #5)
- Subtask 1.1: Create
lib/consent/cookie-consent.ts— typesConsentCategories,ConsentRecord,CONSENT_VERSION,getConsent(),setConsent(),hasAnalyticsConsent(),hasMarketingConsent() - Subtask 1.2: Mirror consent to
document.cookie(memento-cookie-consent=,SameSite=Lax, 1y) for optional SSR read; primary source remainslocalStorage - Subtask 1.3:
hooks/use-cookie-consent.ts— subscribe to storage events for banner hide/show
- Subtask 1.1: Create
- Task 2: UI components (AC: #1, #2, #8)
- Subtask 2.1:
components/legal/cookie-consent-banner.tsx— bottom-fixed, z-index above content, below modals; actions: essentials / reject / manage - Subtask 2.2:
components/legal/cookie-preferences-dialog.tsx— toggles + save; list necessary cookies (session, language, theme) - Subtask 2.3: Mount
<CookieConsentRoot />inapp/layout.tsxinsideSessionProviderWrapperso visitors and authed users both see banner
- Subtask 2.1:
- Task 3: Analytics gate & ErrorReporter policy (AC: #3, #6)
- Subtask 3.1:
lib/analytics/track.ts—trackClientEvent()/trackServerEvent()no-op unless analytics consent (document for future PostHog persaas-deployment-prep.md§D) - Subtask 3.2: Decision (implement): Treat
ErrorReporter→/api/debug/client-erroras strictly necessary (authenticated operational logging only; no third-party). Add code comment inerror-reporter.tsx. Do not send marketing analytics there. - Subtask 3.3: Grep for any
Scriptthird-party loads inapp/— none today; add lint comment intrack.tsthat new scripts must call consent check
- Subtask 3.1:
- Task 4: Settings & i18n (AC: #5, #7, #8)
- Subtask 4.1: Add “Cookie preferences” button to
general-settings-client.tsxopening preferences dialog - Subtask 4.2: On save with analytics on/off, call
updateAISettings({ anonymousAnalytics })when session exists - Subtask 4.3: Add keys under
consent.*(andsettings.cookiePreferencesif needed) in all 15memento-note/locales/*.json - Subtask 4.4: Optional: set footer
legal.link2Hrefto/settings/general#cookiesor anchor — do not build full legal CMS in 4.1
- Subtask 4.1: Add “Cookie preferences” button to
- Task 5: Manual verification (AC: all)
- Subtask 5.1: Clear site data → banner shows on
/and/login - Subtask 5.2: Essentials only → banner hidden; refresh persists; AI/login still works
- Subtask 5.3: Accept analytics →
anonymousAnalyticstrue in DB for logged-in user - Subtask 5.4:
npm run buildinmemento-note/
- Subtask 5.1: Clear site data → banner shows on
Dev Notes
Epic context (Epic 4)
| Story | Scope | Dependency on 4.1 |
|---|---|---|
| 4.1 | Cookie banner + category consent | — |
| 4.2 | Hard account deletion | Independent |
| 4.3 | Data export portability | Independent |
| 4.4 | AI processing consent (just-in-time modal) | Separate UI — do not merge into cookie banner |
| 4.5 | EU data residency | Independent |
| 4.6 | SSO/SAML + audit logs | Independent |
Epic goal: B2B legal blockers for EU buyers. Cookie consent is the first Epic 4 deliverable and unblocks marketing/analytics work later.
Critical brownfield reality
Already in codebase:
UserAISettings.anonymousAnalytics(Boolean @default(false)) — field exists, no UI exposes it today [prisma/schema.prisma].updateAISettingsalready allowsanonymousAnalyticsin allowlist [app/actions/ai-settings.ts].- Functional cookie pattern for language:
localStorage['user-language']+user-languagecookie ingeneral-settings-client.tsxand inline script inapp/layout.tsx. - Footer i18n placeholders for Privacy / Terms / Cookie Policy (
landing.footer.legal.link*) — hrefs still#. ErrorReporterin root layout posts client errors to/api/debug/client-error(auth required) — first-party, not a tracking pixel.
Not implemented (this story):
- No
CookieConsent/ banner components. - No
lib/consent/*or analytics gate. - No PostHog/Umami in
package.json(planned inmemento-note/docs/saas-deployment-prep.mdonly).
Cookie classification (implement exactly)
| Category | Examples in Momento | Consent required |
|---|---|---|
| Strictly necessary | NextAuth session, CSRF (if any), user-language cookie, theme/direction localStorage, consent record itself |
No — always on |
| Analytics | Future PostHog/Umami/Plausible events, product funnel, feature flags tied to identity | Yes — opt-in |
| Marketing | Future ad pixels, retargeting | Yes — opt-in (hide toggle until used) |
Do not block the app shell on analytics rejection — only block non-essential scripts/events.
UX specification (must follow)
From docs/ux-design-specification.md:
- Banner: persistent, thin, bottom-anchored on main UI; actions “Accept essentials” and “Manage” (map “Reject non-essential” to essentials-only path).
- No new top-level pages for consent — overlay/banner + settings re-entry only (Platform Strategy).
- AI consent modal is Story 4.4 — triggered on first AI action, not on login.
Visual: reuse architectural-grid tokens; avoid isolated admin-style topbars.
Files to create
memento-note/
├── lib/consent/cookie-consent.ts # NEW — source of truth
├── lib/analytics/track.ts # NEW — gated no-op + future hook
├── hooks/use-cookie-consent.ts # NEW
├── components/legal/cookie-consent-banner.tsx # NEW
├── components/legal/cookie-preferences-dialog.tsx # NEW
├── components/legal/cookie-consent-root.tsx # NEW — client wrapper
Files to update
| File | Change |
|---|---|
app/layout.tsx |
Render <CookieConsentRoot /> after providers |
app/(main)/settings/general/general-settings-client.tsx |
“Cookie preferences” button |
memento-note/locales/*.json (×15) |
consent.banner.*, consent.preferences.* |
components/error-reporter.tsx |
Comment: necessary cookie / operational, not analytics |
Do not modify unless required: middleware.ts, auth.ts, Stripe/billing routes, BYOK, entitlements.
Suggested consent record shape
type ConsentRecord = {
version: 1
necessary: true // always true
analytics: boolean
marketing: boolean
updatedAt: string // ISO
}
Storage keys: localStorage['memento-consent-v1']; cookie name memento-cookie-consent (JSON base64 or simple flags).
Authenticated user sync
// On accept analytics:
await updateAISettings({ anonymousAnalytics: true })
// On reject:
await updateAISettings({ anonymousAnalytics: false })
Load initial dialog state: if logged in, prefer DB anonymousAnalytics only when local consent missing (first merge); after user sets banner, local record wins until they change preferences again.
Analytics future-proofing
saas-deployment-prep.md documents PostHog (§D) and Umami (docker). For 4.1:
- Implement
hasAnalyticsConsent()check only. - When Epic 3+/growth work adds PostHog, initialize in a client provider inside
if (hasAnalyticsConsent())— EU hosthttps://eu.i.posthog.comwhen configured.
Previous epic intelligence (Epic 3)
| Story | Relevance |
|---|---|
| 3.6 Stripe billing | Billing UI at /settings/billing — unaffected; no Stripe cookies in 4.1 |
| 3.5 BYOK | Unrelated to cookie categories |
| 3.1–3.4 Quotas | Unaffected |
No previous story in Epic 4 (story_num === 1).
Git intelligence (recent)
Recent work focused on design system, brainstorm, billing meter, dead-code cleanup — no consent code in recent commits. Safe greenfield within components/legal/ and lib/consent/.
Project structure notes
- App root:
memento-note/ - Settings layout:
app/(main)/settings/layout.tsx+SettingsNav.tsx - i18n:
lib/i18n/LanguageProvider.tsx, 15 locales underlocales/ - Database safety: No reset; no migration required for 4.1 if using existing
anonymousAnalyticscolumn only
Testing
Per project policy: no new automated tests unless user explicitly requests. Use Task 5 manual checklist only.
References
- [Source: docs/epics.md — Epic 4, Story 4.1]
- [Source: docs/ux-design-specification.md — Flux d'Onboarding Légal, Emplacement Légal]
- [Source: docs/epics.md — NFR-GDPR1]
- [Source: memento-note/docs/saas-deployment-prep.md — §D PostHog/GDPR]
- [Source: memento-note/app/layout.tsx — root scripts & ErrorReporter]
- [Source: memento-note/app/actions/ai-settings.ts — anonymousAnalytics]
- [Source: memento-note/prisma/schema.prisma — UserAISettings.anonymousAnalytics]
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}