docs: add cookies banner & privacy page design spec
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user