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)
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:
- In the cookie banner (as a 4th toggle, like other categories)
- 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_consentstable) for logged-in users - Consent cached in
localStorage(web) orexpo-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_idis NULL for anonymous users (consent tied to session)session_idis NULL for logged-in users (consent tied to account)essentialcategory is always forced totrueby 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).
essentialis always forced totruein the response, regardless of what is sent- For logged-in users: saved with
user_id - For anonymous users: saved with
session_idfrom express-session
Consent Migration on Login/Register
When a user logs in or registers:
- Fetch any consents stored under the current
session_id - Merge with any existing
user_idconsents (user_id takes precedence for conflicts) - Delete session-based consents
- 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— requireshealth_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.tsgets: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.titlecookie_banner.descriptioncookie_banner.category.essentialcookie_banner.category.essential_desccookie_banner.category.analyticscookie_banner.category.analytics_desccookie_banner.category.preferencescookie_banner.category.preferences_desccookie_banner.category.health_datacookie_banner.category.health_data_desccookie_banner.accept_allcookie_banner.reject_optionalcookie_banner.savecookie_banner.more_info
Health Consent Modal
health_consent.titlehealth_consent.descriptionhealth_consent.accepthealth_consent.cancel
Privacy Page
privacy.titleprivacy.section.*(all 9 sections with headings and content)
Navigation
nav.privacy
Privacy Policy Content (9 Sections)
- Data Controller — Hacecalor S.L., contact info
- Data Collected — User profiles (username, email, name, city, avatar), addresses, geolocation, search history, health data (TSI/CIP codes, prescriptions), push notification tokens, session data
- Purpose of Processing — Medicine search, pharmacy locator, availability alerts, health card scanning, app functionality
- Legal Basis — Consent (health data, analytics), legitimate interest (security, fraud prevention), contract (account services)
- External Services — CIMA API (Spanish Medicines Agency), Grafana Faro (analytics), Nominatim/OpenStreetMap (geocoding), N8N (automation), email provider (password reset)
- Data Retention — Session: 24h, Account: until deletion, Consent records: 3 years, Search history: 6 months
- User Rights — Access, rectification, erasure, portability, objection, withdraw consent
- Contact — How to exercise rights / contact the data controller
- Cookie Policy — List of cookies by category, purpose, duration
Implementation Phases
Phase 1: Backend + Consent Infrastructure
- Create
user_consentsDB migration (PostgreSQL + SQLite) - Add
GET/PUT /api/consentsendpoints - 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.