diff --git a/docs/superpowers/specs/2026-08-26-cookies-banner-privacy-design.md b/docs/superpowers/specs/2026-08-26-cookies-banner-privacy-design.md new file mode 100644 index 0000000..027e4d8 --- /dev/null +++ b/docs/superpowers/specs/2026-08-26-cookies-banner-privacy-design.md @@ -0,0 +1,316 @@ +# 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.