# 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) --- ## Consent Categories | 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` ```sql 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: ```json { "essential": true, "analytics": false, "preferences": true, "health_data": false } ``` #### PUT /api/consents Request: ```json { "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 ### Consent Migration on Login/Register 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` #### Footer / BottomNav update - Adds "Política de privacidad" link to existing navigation - Opens PrivacyView ### Consent Sync Utility - `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 ### Consent Sync Utility - `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 - `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 Modal - `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 ### Phase 1: Backend + Consent Infrastructure - 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.