# 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 1. [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**. 2. [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. 3. [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. 4. [AC4] **Persistence:** Consent is stored client-side (`localStorage` + optional `memento-cookie-consent` cookie for SSR hints) with schema version, timestamp, and category flags. Re-opening preferences from Settings updates the same record. 5. [AC5] **Authenticated sync:** When a logged-in user accepts analytics, persist `UserAISettings.anonymousAnalytics = true` via existing `updateAISettings`. On reject, set `false`. Guests rely on local storage only. 6. [AC6] **Analytics gate:** Introduce a single choke-point `lib/analytics/track.ts` (or `lib/consent/analytics.ts`) where all future product analytics calls no-op unless `hasAnalyticsConsent()`. Do **not** add PostHog/Umami packages in this story; wire `ErrorReporter` and any future trackers through the gate policy documented below. 7. [AC7] **Settings re-entry:** **Settings → General** (or About) includes a “Cookie preferences” control that reopens the same manage UI without clearing prior choices. 8. [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; matches `docs/ux-design-specification.md` §Emplacement Légal. 9. [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. 10. [AC10] **No destructive DB:** No `prisma migrate reset`. If schema change is needed for consent audit log, prefer **no migration** in 4.1 (client + `anonymousAnalytics` only). --- ## Tasks / Subtasks - [ ] Task 1: Consent model & utilities (AC: #2, #4, #5) - [ ] Subtask 1.1: Create `lib/consent/cookie-consent.ts` — types `ConsentCategories`, `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 remains `localStorage` - [ ] Subtask 1.3: `hooks/use-cookie-consent.ts` — subscribe to storage events for banner hide/show - [ ] 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 `` in `app/layout.tsx` inside `SessionProviderWrapper` so visitors and authed users both see banner - [ ] 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 per `saas-deployment-prep.md` §D) - [ ] Subtask 3.2: **Decision (implement):** Treat `ErrorReporter` → `/api/debug/client-error` as **strictly necessary** (authenticated operational logging only; no third-party). Add code comment in `error-reporter.tsx`. Do not send marketing analytics there. - [ ] Subtask 3.3: Grep for any `Script` third-party loads in `app/` — none today; add lint comment in `track.ts` that new scripts must call consent check - [ ] Task 4: Settings & i18n (AC: #5, #7, #8) - [ ] Subtask 4.1: Add “Cookie preferences” button to `general-settings-client.tsx` opening preferences dialog - [ ] Subtask 4.2: On save with analytics on/off, call `updateAISettings({ anonymousAnalytics })` when session exists - [ ] Subtask 4.3: Add keys under `consent.*` (and `settings.cookiePreferences` if needed) in **all 15** `memento-note/locales/*.json` - [ ] Subtask 4.4: Optional: set footer `legal.link2Href` to `/settings/general#cookies` or anchor — do not build full legal CMS in 4.1 - [ ] 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 → `anonymousAnalytics` true in DB for logged-in user - [ ] Subtask 5.4: `npm run build` in `memento-note/` --- ## 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`]. - `updateAISettings` already allows `anonymousAnalytics` in allowlist [`app/actions/ai-settings.ts`]. - Functional cookie pattern for language: `localStorage['user-language']` + `user-language` cookie in `general-settings-client.tsx` and inline script in `app/layout.tsx`. - Footer i18n placeholders for Privacy / Terms / Cookie Policy (`landing.footer.legal.link*`) — hrefs still `#`. - `ErrorReporter` in 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 in `memento-note/docs/saas-deployment-prep.md` only). ### Cookie classification (implement exactly) | Category | Examples in Memento | 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 `` 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 ```typescript 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 ```typescript // 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 host `https://eu.i.posthog.com` when 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 under `locales/` - **Database safety:** No reset; no migration required for 4.1 if using existing `anonymousAnalytics` column 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}} ### Debug Log References ### Completion Notes List ### File List