Files
FarmaFinder/docs/superpowers/specs/2026-08-26-cookies-banner-privacy-design.md
T
2026-08-26 13:06:39 +02:00

10 KiB

Design: Cookies Banner & Privacy Page

Overview

Add GDPR/RGPD-compliant cookie consent management, a health data consent flow, and a privacy policy page to FarmaFinder across both web and mobile platforms. The app collects significant personal data including health/medical data (TSI card scanning), requiring special category consent under GDPR Article 9.

Company/Data Controller: Hacecalor S.L.

Languages: Spanish (primary) + Catalan

Platforms: Web frontend (apps/frontend) + Mobile frontend (apps/frontend-mobile)


Category Required Default Description
essential Yes (always on) ON Session cookie (express-session, HTTP-only), CSRF, authentication
analytics No (opt-in) OFF Grafana Faro (browser RUM, Web Vitals, error tracking)
preferences No (opt-in) OFF Theme (dark/light), language (es/ca), saved searches
health_data No (opt-in) OFF TSI card scanning, CIP code extraction, prescription lookup

health_data consent is shown in two places:

  1. In the cookie banner (as a 4th toggle, like other categories)
  2. Re-prompted via a dedicated modal when the user first attempts to scan their TSI card

If the user already accepted health_data via the banner, the TSI modal is skipped.


Architecture

Approach: Server-side consent + client sync

  • Consent stored in database (user_consents table) for logged-in users
  • Consent cached in localStorage (web) or expo-secure-store (mobile) for fast access and anonymous users
  • On login/register: Anonymous consents migrated to the user account
  • Backend can enforce consent via middleware

Database Schema

New table: user_consents

CREATE TABLE user_consents (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    session_id VARCHAR(255),
    category VARCHAR(20) NOT NULL CHECK (category IN ('essential', 'analytics', 'preferences', 'health_data')),
    granted BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, category),
    UNIQUE(session_id, category)
);

CREATE INDEX idx_user_consents_user_id ON user_consents(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_user_consents_session_id ON user_consents(session_id) WHERE session_id IS NOT NULL;
  • user_id is NULL for anonymous users (consent tied to session)
  • session_id is NULL for logged-in users (consent tied to account)
  • essential category is always forced to true by the backend

Backend API

Endpoints

Method Path Auth Description
GET /api/consents Optional Get current user's/session's consents
PUT /api/consents Optional Save consent preferences

GET /api/consents

Response:

{
  "essential": true,
  "analytics": false,
  "preferences": true,
  "health_data": false
}

PUT /api/consents

Request:

{
  "categories": {
    "analytics": true,
    "preferences": false,
    "health_data": true
  }
}

Response: Same as GET (full consent state after save).

  • essential is always forced to true in the response, regardless of what is sent
  • For logged-in users: saved with user_id
  • For anonymous users: saved with session_id from express-session

When a user logs in or registers:

  1. Fetch any consents stored under the current session_id
  2. Merge with any existing user_id consents (user_id takes precedence for conflicts)
  3. Delete session-based consents
  4. Return merged consents

Middleware

requireConsent(category) — checks if the current user/session has granted consent for the given category. Returns 403 if not granted.

Applied to:

  • POST /api/tsi/scan — requires health_data

Web Frontend (apps/frontend)

New Components

CookieBanner.jsx + CookieBanner.css

  • Fixed banner at bottom of screen, shown on first visit (no consent record in localStorage)
  • Brief explanation text
  • 4 category toggles: Esenciales (locked ON), Analítica, Preferencias, Datos de Salud
  • Buttons: "Aceptar todo" / "Rechazar opcionales" / "Guardar"
  • "Más información" link → opens PrivacyView
  • Styled with existing CSS custom properties (Material Design 3 tokens)
  • Responsive: full-width on mobile, centered card on desktop

HealthConsentModal.jsx + HealthConsentModal.css

  • Modal shown when user first taps TSI scanner (if health_data consent not yet granted)
  • Explains what data is collected (CIP code, prescriptions) and how it's used
  • Buttons: "Aceptar y escanear" / "Cancelar"
  • Sets health_data consent on accept, then proceeds to scan
  • Skipped if user previously accepted via banner

PrivacyView.jsx + PrivacyView.css

  • Full privacy policy page, new screen in SPA routing
  • Sections: Data Controller, Data Collected, Purpose, Legal Basis, External Services, Retention, User Rights, Contact, Cookie Policy
  • Content in Spanish with Catalan toggle (using existing i18n pattern)
  • Accessible via route privacy
  • Adds "Política de privacidad" link to existing navigation
  • Opens PrivacyView
  • utils/consent.js — manages localStorage ↔ API sync
  • On app load: fetch from API if logged in, else read localStorage
  • On consent change: update localStorage + call PUT /api/consents
  • Grafana Faro initialization gated on analytics consent

Grafana Faro Gating

In utils/faro.js:

  • Check consent before calling init()
  • If analytics consent not granted, Faro is not initialized
  • If consent is granted later (via banner), re-initialize Faro

TSI Scanner Gating

In ScannerView.jsx:

  • Before opening TSI scan, check health_data consent
  • If not granted, show HealthConsentModal
  • If granted, proceed directly to scan

Mobile Frontend (apps/frontend-mobile)

New Components

components/CookieBanner.tsx

  • Bottom sheet / slide-up panel with same 4 categories
  • Same toggle logic as web
  • Buttons: "Aceptar todo" / "Rechazar opcionales" / "Guardar"
  • Styled with ThemeProvider tokens (dark/light theme)
  • Shown on first launch (checked via expo-secure-store key consents_initialized)

components/HealthConsentModal.tsx

  • Same purpose as web: shown when user first tries to scan TSI
  • Explains data collection, two buttons
  • Sets consent via API, then proceeds to scanner
  • Skipped if consent already granted

app/privacy.tsx (or app/(tabs)/privacy.tsx)

  • New Expo Router screen for privacy policy
  • Same content structure as web
  • Uses ScrollView for long content
  • Accessible from profile screen
  • services/consent.ts — manages expo-secure-store ↔ API sync
  • On app load: fetch from API if logged in, else read secure-store
  • On consent change: update secure-store + call PUT /api/consents

API Client Additions

  • services/api.ts gets: getConsents(), updateConsents(categories)
  • Same endpoints as web

TSI Scanner Gating

In scanner flow:

  • Before opening TSI scan, check health_data consent
  • If not granted, show HealthConsentModal
  • If granted, proceed directly

i18n Keys

  • cookie_banner.title
  • cookie_banner.description
  • cookie_banner.category.essential
  • cookie_banner.category.essential_desc
  • cookie_banner.category.analytics
  • cookie_banner.category.analytics_desc
  • cookie_banner.category.preferences
  • cookie_banner.category.preferences_desc
  • cookie_banner.category.health_data
  • cookie_banner.category.health_data_desc
  • cookie_banner.accept_all
  • cookie_banner.reject_optional
  • cookie_banner.save
  • cookie_banner.more_info
  • health_consent.title
  • health_consent.description
  • health_consent.accept
  • health_consent.cancel

Privacy Page

  • privacy.title
  • privacy.section.* (all 9 sections with headings and content)

Navigation

  • nav.privacy

Privacy Policy Content (9 Sections)

  1. Data Controller — Hacecalor S.L., contact info
  2. Data Collected — User profiles (username, email, name, city, avatar), addresses, geolocation, search history, health data (TSI/CIP codes, prescriptions), push notification tokens, session data
  3. Purpose of Processing — Medicine search, pharmacy locator, availability alerts, health card scanning, app functionality
  4. Legal Basis — Consent (health data, analytics), legitimate interest (security, fraud prevention), contract (account services)
  5. External Services — CIMA API (Spanish Medicines Agency), Grafana Faro (analytics), Nominatim/OpenStreetMap (geocoding), N8N (automation), email provider (password reset)
  6. Data Retention — Session: 24h, Account: until deletion, Consent records: 3 years, Search history: 6 months
  7. User Rights — Access, rectification, erasure, portability, objection, withdraw consent
  8. Contact — How to exercise rights / contact the data controller
  9. Cookie Policy — List of cookies by category, purpose, duration

Implementation Phases

  • Create user_consents DB migration (PostgreSQL + SQLite)
  • Add GET/PUT /api/consents endpoints
  • Add requireConsent() middleware
  • Add consent migration on login/register
  • Deliverable: Consent API working

Phase 2: Web Frontend

  • CookieBanner.jsx + CSS
  • HealthConsentModal.jsx + CSS
  • PrivacyView.jsx + CSS
  • Footer/nav link to privacy page
  • Consent sync utility (localStorage ↔ API)
  • Grafana Faro gated on analytics consent
  • TSI scanner gated on health_data consent
  • i18n keys (es + ca)
  • Deliverable: Full web consent flow working

Phase 3: Mobile Frontend

  • CookieBanner.tsx
  • HealthConsentModal.tsx
  • privacy.tsx screen
  • Consent sync utility (expo-secure-store ↔ API)
  • API client methods (getConsents, updateConsents)
  • TSI scanner gated on health_data consent
  • i18n keys (es + ca)
  • Deliverable: Full mobile consent flow working

Final: Privacy Policy Content

  • Write full Spanish text for all 9 sections
  • Write full Catalan translation
  • Add to both web and mobile i18n files

Open Questions

None — all decisions finalized during brainstorming.