feat(mobile): add complete consent flow (banner, health modal, privacy, i18n)

This commit is contained in:
Antoni Nuñez Romeu
2026-08-26 15:24:09 +02:00
parent 1f918be99d
commit 80c473e439
9 changed files with 350 additions and 2 deletions
@@ -379,6 +379,14 @@ export default function ProfileScreen() {
</TouchableOpacity>
</>
)}
<View style={[styles.menuDivider, { backgroundColor: colors.surfaceLow }]} />
<TouchableOpacity style={styles.menuRow} onPress={() => router.push('/privacy')}>
<View style={[styles.menuIconCircle, { backgroundColor: colors.surfaceLow }]}>
<Ionicons name="shield-checkmark-outline" size={20} color={colors.primary} />
</View>
<Text style={[styles.menuLabel, { color: colors.text }]}>{t('nav.privacy')}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
</View>
{/* Search history */}
+20 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Image, StyleSheet, View } from 'react-native';
@@ -10,6 +10,8 @@ import { registerForPushNotifications, addNotificationListener, addNotificationR
import { ThemeProvider, useThemeContext } from '../components/ThemeProvider';
import { LanguageProvider, useTranslation } from '../src/i18n';
import { initFaro } from '../services/faro';
import { hasConsentChoice } from '../services/consent';
import CookieBanner from '../components/CookieBanner';
// Boot Faro RUM once, as early as possible.
initFaro();
@@ -23,6 +25,7 @@ function RootLayoutInner() {
const { checkAuth } = useAuthStore();
const { colors, isDark } = useThemeContext();
const { t } = useTranslation();
const [showCookieBanner, setShowCookieBanner] = useState(false);
const notificationListener = useRef<ReturnType<typeof addNotificationListener>>();
const responseListener = useRef<ReturnType<typeof addNotificationResponseListener>>();
@@ -31,6 +34,11 @@ function RootLayoutInner() {
registerForPushNotifications();
(async () => {
const consentChoice = await hasConsentChoice();
if (!consentChoice) setShowCookieBanner(true);
})();
notificationListener.current = addNotificationListener((notification) => {
console.log('Notification received:', notification);
});
@@ -85,7 +93,18 @@ function RootLayoutInner() {
headerShown: false,
}}
/>
<Stack.Screen name="privacy" options={{ title: 'Política de Privacidad' }} />
</Stack>
{showCookieBanner && (
<CookieBanner
onConsent={async (consents) => {
const { saveConsents } = await import('../services/consent');
await saveConsents(consents);
setShowCookieBanner(false);
}}
onPrivacyPress={() => setShowCookieBanner(false)}
/>
)}
<StatusBar style={isDark ? 'light' : 'auto'} />
</>
);
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useThemeContext } from '../components/ThemeProvider';
import { useTranslation } from '../src/i18n';
import { spacing, borderRadius } from '../constants/theme';
const SECTIONS = ['controller', 'data_collected', 'purpose', 'legal_basis', 'external_services', 'retention', 'rights', 'contact', 'cookies'] as const;
export default function PrivacyScreen() {
const router = useRouter();
const { colors } = useThemeContext();
const { t } = useTranslation();
return (
<ScrollView style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: colors.card }]}>
<Ionicons name="arrow-back" size={20} color={colors.text} />
</TouchableOpacity>
<Text style={[styles.title, { color: colors.text }]}>{t('privacy.title')}</Text>
</View>
<Text style={[styles.updated, { color: colors.textSecondary }]}>{t('privacy.last_updated')}</Text>
{SECTIONS.map(section => (
<View key={section} style={[styles.section, { backgroundColor: colors.card }]}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t(`privacy.section.${section}.title`)}</Text>
<Text style={[styles.sectionContent, { color: colors.textSecondary }]}>{t(`privacy.section.${section}.content`)}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: spacing.md },
header: { flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 16 },
backBtn: { width: 36, height: 36, borderRadius: borderRadius.md, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 22, fontWeight: '700' },
updated: { fontSize: 12, marginBottom: 16 },
section: { borderRadius: borderRadius.md, padding: 16, marginBottom: 12 },
sectionTitle: { fontSize: 16, fontWeight: '700', marginBottom: 8 },
sectionContent: { fontSize: 14, lineHeight: 22 },
});
+33 -1
View File
@@ -3,12 +3,16 @@ import { View, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
import { BarcodeScanner } from '../components/BarcodeScanner';
import { searchMedicines } from '../services/medicines';
import { hasConsent } from '../services/consent';
import HealthConsentModal from '../components/HealthConsentModal';
export default function ScannerScreen() {
const router = useRouter();
const [isSearching, setIsSearching] = useState(false);
const [showHealthConsent, setShowHealthConsent] = useState(false);
const [pendingBarcode, setPendingBarcode] = useState<string | null>(null);
const handleBarcodeScanned = async (barcode: string) => {
const processBarcode = async (barcode: string) => {
setIsSearching(true);
try {
const results = await searchMedicines(barcode);
@@ -25,6 +29,16 @@ export default function ScannerScreen() {
}
};
const handleBarcodeScanned = async (barcode: string) => {
const healthConsent = await hasConsent('health_data');
if (!healthConsent) {
setPendingBarcode(barcode);
setShowHealthConsent(true);
return;
}
await processBarcode(barcode);
};
const handleClose = () => {
router.back();
};
@@ -35,6 +49,24 @@ export default function ScannerScreen() {
onBarcodeScanned={handleBarcodeScanned}
onClose={handleClose}
/>
{showHealthConsent && (
<HealthConsentModal
onAccept={async () => {
setShowHealthConsent(false);
if (pendingBarcode) {
const { saveConsents, getLocalConsents } = await import('../services/consent');
const current = await getLocalConsents();
await saveConsents({ ...current, health_data: true });
await processBarcode(pendingBarcode);
setPendingBarcode(null);
}
}}
onCancel={() => {
setShowHealthConsent(false);
setPendingBarcode(null);
}}
/>
)}
</View>
);
}