Privacy settings #60
@@ -379,6 +379,14 @@ export default function ProfileScreen() {
|
|||||||
</TouchableOpacity>
|
</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>
|
</View>
|
||||||
|
|
||||||
{/* Search history */}
|
{/* Search history */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Image, StyleSheet, View } from 'react-native';
|
import { Image, StyleSheet, View } from 'react-native';
|
||||||
@@ -10,6 +10,8 @@ import { registerForPushNotifications, addNotificationListener, addNotificationR
|
|||||||
import { ThemeProvider, useThemeContext } from '../components/ThemeProvider';
|
import { ThemeProvider, useThemeContext } from '../components/ThemeProvider';
|
||||||
import { LanguageProvider, useTranslation } from '../src/i18n';
|
import { LanguageProvider, useTranslation } from '../src/i18n';
|
||||||
import { initFaro } from '../services/faro';
|
import { initFaro } from '../services/faro';
|
||||||
|
import { hasConsentChoice } from '../services/consent';
|
||||||
|
import CookieBanner from '../components/CookieBanner';
|
||||||
|
|
||||||
// Boot Faro RUM once, as early as possible.
|
// Boot Faro RUM once, as early as possible.
|
||||||
initFaro();
|
initFaro();
|
||||||
@@ -23,6 +25,7 @@ function RootLayoutInner() {
|
|||||||
const { checkAuth } = useAuthStore();
|
const { checkAuth } = useAuthStore();
|
||||||
const { colors, isDark } = useThemeContext();
|
const { colors, isDark } = useThemeContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [showCookieBanner, setShowCookieBanner] = useState(false);
|
||||||
const notificationListener = useRef<ReturnType<typeof addNotificationListener>>();
|
const notificationListener = useRef<ReturnType<typeof addNotificationListener>>();
|
||||||
const responseListener = useRef<ReturnType<typeof addNotificationResponseListener>>();
|
const responseListener = useRef<ReturnType<typeof addNotificationResponseListener>>();
|
||||||
|
|
||||||
@@ -31,6 +34,11 @@ function RootLayoutInner() {
|
|||||||
|
|
||||||
registerForPushNotifications();
|
registerForPushNotifications();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const consentChoice = await hasConsentChoice();
|
||||||
|
if (!consentChoice) setShowCookieBanner(true);
|
||||||
|
})();
|
||||||
|
|
||||||
notificationListener.current = addNotificationListener((notification) => {
|
notificationListener.current = addNotificationListener((notification) => {
|
||||||
console.log('Notification received:', notification);
|
console.log('Notification received:', notification);
|
||||||
});
|
});
|
||||||
@@ -85,7 +93,18 @@ function RootLayoutInner() {
|
|||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen name="privacy" options={{ title: 'Política de Privacidad' }} />
|
||||||
</Stack>
|
</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'} />
|
<StatusBar style={isDark ? 'light' : 'auto'} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
});
|
||||||
@@ -3,12 +3,16 @@ import { View, StyleSheet } from 'react-native';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { BarcodeScanner } from '../components/BarcodeScanner';
|
import { BarcodeScanner } from '../components/BarcodeScanner';
|
||||||
import { searchMedicines } from '../services/medicines';
|
import { searchMedicines } from '../services/medicines';
|
||||||
|
import { hasConsent } from '../services/consent';
|
||||||
|
import HealthConsentModal from '../components/HealthConsentModal';
|
||||||
|
|
||||||
export default function ScannerScreen() {
|
export default function ScannerScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
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);
|
setIsSearching(true);
|
||||||
try {
|
try {
|
||||||
const results = await searchMedicines(barcode);
|
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 = () => {
|
const handleClose = () => {
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
@@ -35,6 +49,24 @@ export default function ScannerScreen() {
|
|||||||
onBarcodeScanned={handleBarcodeScanned}
|
onBarcodeScanned={handleBarcodeScanned}
|
||||||
onClose={handleClose}
|
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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal } from 'react-native';
|
||||||
|
import { useThemeContext } from './ThemeProvider';
|
||||||
|
import { useTranslation } from '../src/i18n';
|
||||||
|
import { borderRadius } from '../constants/theme';
|
||||||
|
|
||||||
|
interface CookieBannerProps {
|
||||||
|
onConsent: (consents: { essential: boolean; analytics: boolean; preferences: boolean; health_data: boolean }) => void;
|
||||||
|
onPrivacyPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CookieBanner({ onConsent, onPrivacyPress }: CookieBannerProps) {
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [categories, setCategories] = useState({ analytics: false, preferences: false, health_data: false });
|
||||||
|
|
||||||
|
const toggleCategory = (cat: keyof typeof categories) => {
|
||||||
|
setCategories(prev => ({ ...prev, [cat]: !prev[cat] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryKeys = ['analytics', 'preferences', 'health_data'] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal transparent animationType="slide" visible>
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={[styles.title, { color: colors.text }]}>{t('cookie_banner.title')}</Text>
|
||||||
|
<Text style={[styles.desc, { color: colors.textSecondary }]}>{t('cookie_banner.description')}</Text>
|
||||||
|
<View style={styles.categories}>
|
||||||
|
<View style={[styles.category, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={styles.categoryInfo}>
|
||||||
|
<Text style={[styles.categoryName, { color: colors.text }]}>{t('cookie_banner.category.essential')}</Text>
|
||||||
|
<Text style={[styles.categoryDesc, { color: colors.textSecondary }]}>{t('cookie_banner.category.essential_desc')}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.toggle, styles.toggleLocked, { backgroundColor: colors.primary }]}>
|
||||||
|
<Text style={styles.toggleLabel}>ON</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{categoryKeys.map(cat => (
|
||||||
|
<View key={cat} style={[styles.category, { backgroundColor: colors.background }]}>
|
||||||
|
<View style={styles.categoryInfo}>
|
||||||
|
<Text style={[styles.categoryName, { color: colors.text }]}>{t(`cookie_banner.category.${cat}`)}</Text>
|
||||||
|
<Text style={[styles.categoryDesc, { color: colors.textSecondary }]}>{t(`cookie_banner.category.${cat}_desc`)}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={[styles.toggle, categories[cat] && { backgroundColor: colors.primary }]} onPress={() => toggleCategory(cat)} activeOpacity={0.7}>
|
||||||
|
<View style={[styles.toggleThumb, categories[cat] && styles.toggleThumbOn]} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View style={styles.actions}>
|
||||||
|
<TouchableOpacity style={[styles.btn, { backgroundColor: colors.primary }]} onPress={() => onConsent({ essential: true, analytics: true, preferences: true, health_data: true })}>
|
||||||
|
<Text style={[styles.btnText, { color: colors.background }]}>{t('cookie_banner.accept_all')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.btn, { backgroundColor: colors.background, borderColor: colors.border, borderWidth: 1 }]} onPress={() => onConsent({ essential: true, analytics: false, preferences: false, health_data: false })}>
|
||||||
|
<Text style={[styles.btnText, { color: colors.text }]}>{t('cookie_banner.reject_optional')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.btn, { borderColor: colors.primary, borderWidth: 1 }]} onPress={() => onConsent({ essential: true, ...categories })}>
|
||||||
|
<Text style={[styles.btnText, { color: colors.primary }]}>{t('cookie_banner.save')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity onPress={onPrivacyPress}>
|
||||||
|
<Text style={[styles.moreInfo, { color: colors.textSecondary }]}>{t('cookie_banner.more_info')} →</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
||||||
|
container: { borderTopLeftRadius: borderRadius.lg, borderTopRightRadius: borderRadius.lg, padding: 20, maxHeight: '85%' },
|
||||||
|
title: { fontSize: 18, fontWeight: '700', marginBottom: 8 },
|
||||||
|
desc: { fontSize: 14, lineHeight: 20, marginBottom: 16 },
|
||||||
|
categories: { gap: 10, marginBottom: 16 },
|
||||||
|
category: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 12, borderRadius: borderRadius.md, gap: 12 },
|
||||||
|
categoryInfo: { flex: 1, gap: 2 },
|
||||||
|
categoryName: { fontSize: 14, fontWeight: '600' },
|
||||||
|
categoryDesc: { fontSize: 12, lineHeight: 16 },
|
||||||
|
toggle: { width: 44, height: 24, borderRadius: 12, backgroundColor: '#ccc', justifyContent: 'center', alignItems: 'center', padding: 2 },
|
||||||
|
toggleLocked: { opacity: 0.6 },
|
||||||
|
toggleLabel: { fontSize: 9, fontWeight: '700', color: 'white' },
|
||||||
|
toggleThumb: { width: 20, height: 20, borderRadius: 10, backgroundColor: 'white', alignSelf: 'flex-start' },
|
||||||
|
toggleThumbOn: { alignSelf: 'flex-end' },
|
||||||
|
actions: { gap: 8, marginBottom: 12 },
|
||||||
|
btn: { paddingVertical: 12, borderRadius: 999, alignItems: 'center' },
|
||||||
|
btnText: { fontSize: 14, fontWeight: '600' },
|
||||||
|
moreInfo: { textAlign: 'center', fontSize: 12, textDecorationLine: 'underline' },
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet, Modal } from 'react-native';
|
||||||
|
import { useThemeContext } from './ThemeProvider';
|
||||||
|
import { useTranslation } from '../src/i18n';
|
||||||
|
import { borderRadius } from '../constants/theme';
|
||||||
|
|
||||||
|
interface HealthConsentModalProps {
|
||||||
|
onAccept: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HealthConsentModal({ onAccept, onCancel }: HealthConsentModalProps) {
|
||||||
|
const { colors } = useThemeContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal transparent animationType="fade" visible>
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||||
|
<Text style={[styles.title, { color: colors.text }]}>{t('health_consent.title')}</Text>
|
||||||
|
<Text style={[styles.desc, { color: colors.textSecondary }]}>{t('health_consent.description')}</Text>
|
||||||
|
<View style={styles.actions}>
|
||||||
|
<TouchableOpacity style={[styles.btn, { backgroundColor: colors.primary }]} onPress={onAccept}>
|
||||||
|
<Text style={[styles.btnText, { color: colors.background }]}>{t('health_consent.accept')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.btn, { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border }]} onPress={onCancel}>
|
||||||
|
<Text style={[styles.btnText, { color: colors.text }]}>{t('health_consent.cancel')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', padding: 24 },
|
||||||
|
container: { borderRadius: borderRadius.lg, padding: 24, width: '100%', maxWidth: 340 },
|
||||||
|
title: { fontSize: 18, fontWeight: '700', marginBottom: 12 },
|
||||||
|
desc: { fontSize: 14, lineHeight: 20, marginBottom: 20 },
|
||||||
|
actions: { gap: 10 },
|
||||||
|
btn: { paddingVertical: 12, borderRadius: 999, alignItems: 'center' },
|
||||||
|
btnText: { fontSize: 14, fontWeight: '600' },
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import { api } from './api';
|
||||||
|
|
||||||
|
const CONSENT_KEY = 'consents';
|
||||||
|
|
||||||
|
export interface Consents {
|
||||||
|
essential: boolean;
|
||||||
|
analytics: boolean;
|
||||||
|
preferences: boolean;
|
||||||
|
health_data: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONSENTS: Consents = {
|
||||||
|
essential: true,
|
||||||
|
analytics: false,
|
||||||
|
preferences: false,
|
||||||
|
health_data: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getLocalConsents(): Promise<Consents> {
|
||||||
|
try {
|
||||||
|
const stored = await SecureStore.getItemAsync(CONSENT_KEY);
|
||||||
|
if (stored) return { ...DEFAULT_CONSENTS, ...JSON.parse(stored), essential: true };
|
||||||
|
} catch {}
|
||||||
|
return { ...DEFAULT_CONSENTS };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setLocalConsents(consents: Consents): Promise<void> {
|
||||||
|
try { await SecureStore.setItemAsync(CONSENT_KEY, JSON.stringify(consents)); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasConsentChoice(): Promise<boolean> {
|
||||||
|
try { return (await SecureStore.getItemAsync(CONSENT_KEY)) !== null; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchServerConsents(): Promise<Consents | null> {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/consents');
|
||||||
|
await setLocalConsents(res.data);
|
||||||
|
return res.data;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveConsents(consents: Consents): Promise<Consents> {
|
||||||
|
const toSave = { ...consents, essential: true };
|
||||||
|
await setLocalConsents(toSave);
|
||||||
|
try {
|
||||||
|
const res = await api.put('/consents', {
|
||||||
|
categories: { analytics: toSave.analytics, preferences: toSave.preferences, health_data: toSave.health_data },
|
||||||
|
});
|
||||||
|
await setLocalConsents(res.data);
|
||||||
|
return res.data;
|
||||||
|
} catch { return toSave; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasConsent(category: keyof Consents): Promise<boolean> {
|
||||||
|
const consents = await getLocalConsents();
|
||||||
|
return consents[category] === true;
|
||||||
|
}
|
||||||
@@ -230,6 +230,31 @@ const ca = {
|
|||||||
'barcodeScanner.cancel': 'Cancel·lar',
|
'barcodeScanner.cancel': 'Cancel·lar',
|
||||||
'barcodeScanner.scanningHint': 'Apunteu la càmera al codi de barres del medicament',
|
'barcodeScanner.scanningHint': 'Apunteu la càmera al codi de barres del medicament',
|
||||||
'barcodeScanner.scanAgain': 'Escanejar de nou',
|
'barcodeScanner.scanAgain': 'Escanejar de nou',
|
||||||
|
|
||||||
|
// Cookie Banner
|
||||||
|
'cookie_banner.title': 'Utilitzem cookies i dades personals',
|
||||||
|
'cookie_banner.description': 'Utilitzem cookies i tecnologies similars per millorar la teva experiència i, si ho permetes, escanejar la teva targeta sanitària.',
|
||||||
|
'cookie_banner.category.essential': 'Essencials',
|
||||||
|
'cookie_banner.category.essential_desc': 'Necessàries per al funcionament de l\'app.',
|
||||||
|
'cookie_banner.category.analytics': 'Analítica',
|
||||||
|
'cookie_banner.category.analytics_desc': 'Ens ajuden a millorar l\'app.',
|
||||||
|
'cookie_banner.category.preferences': 'Preferències',
|
||||||
|
'cookie_banner.category.preferences_desc': 'Recordar el teu idioma i tema.',
|
||||||
|
'cookie_banner.category.health_data': 'Dades de salut',
|
||||||
|
'cookie_banner.category.health_data_desc': 'Escaneig de targeta sanitària.',
|
||||||
|
'cookie_banner.accept_all': 'Acceptar tot',
|
||||||
|
'cookie_banner.reject_optional': 'Rebutjar',
|
||||||
|
'cookie_banner.save': 'Desar',
|
||||||
|
'cookie_banner.more_info': 'Més informació',
|
||||||
|
// Health Consent
|
||||||
|
'health_consent.title': 'Consentiment per a dades de salut',
|
||||||
|
'health_consent.description': 'Per escanejar la teva targeta sanitària (TSI), necessitem extreure el teu codi CIP i accedir a les teves receptes.',
|
||||||
|
'health_consent.accept': 'Acceptar i escanejar',
|
||||||
|
'health_consent.cancel': 'Cancel·lar',
|
||||||
|
// Privacy
|
||||||
|
'privacy.title': 'Política de Privacitat',
|
||||||
|
'privacy.last_updated': 'Última actualització: 26/08/2026',
|
||||||
|
'nav.privacy': 'Política de privacitat',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ca;
|
export default ca;
|
||||||
|
|||||||
@@ -230,6 +230,31 @@ const es = {
|
|||||||
'barcodeScanner.cancel': 'Cancelar',
|
'barcodeScanner.cancel': 'Cancelar',
|
||||||
'barcodeScanner.scanningHint': 'Apunta la cámara al código de barras del medicamento',
|
'barcodeScanner.scanningHint': 'Apunta la cámara al código de barras del medicamento',
|
||||||
'barcodeScanner.scanAgain': 'Escanear de nuevo',
|
'barcodeScanner.scanAgain': 'Escanear de nuevo',
|
||||||
|
|
||||||
|
// Cookie Banner
|
||||||
|
'cookie_banner.title': 'Utilizamos cookies y datos personales',
|
||||||
|
'cookie_banner.description': 'Utilizamos cookies y tecnologías similares para mejorar tu experiencia, analizar el uso de la app y, si lo permites, escanear tu tarjeta sanitaria.',
|
||||||
|
'cookie_banner.category.essential': 'Esenciales',
|
||||||
|
'cookie_banner.category.essential_desc': 'Necesarias para el funcionamiento de la app.',
|
||||||
|
'cookie_banner.category.analytics': 'Analítica',
|
||||||
|
'cookie_banner.category.analytics_desc': 'Nos ayudan a mejorar la app.',
|
||||||
|
'cookie_banner.category.preferences': 'Preferencias',
|
||||||
|
'cookie_banner.category.preferences_desc': 'Recordar tu idioma y tema.',
|
||||||
|
'cookie_banner.category.health_data': 'Datos de salud',
|
||||||
|
'cookie_banner.category.health_data_desc': 'Escaneo de tarjeta sanitaria.',
|
||||||
|
'cookie_banner.accept_all': 'Aceptar todo',
|
||||||
|
'cookie_banner.reject_optional': 'Rechazar',
|
||||||
|
'cookie_banner.save': 'Guardar',
|
||||||
|
'cookie_banner.more_info': 'Más información',
|
||||||
|
// Health Consent
|
||||||
|
'health_consent.title': 'Consentimiento para datos de salud',
|
||||||
|
'health_consent.description': 'Para escanear tu tarjeta sanitaria (TSI), necesitamos extraer tu código CIP y acceder a tus recetas.',
|
||||||
|
'health_consent.accept': 'Aceptar y escanear',
|
||||||
|
'health_consent.cancel': 'Cancelar',
|
||||||
|
// Privacy
|
||||||
|
'privacy.title': 'Política de Privacidad',
|
||||||
|
'privacy.last_updated': 'Última actualización: 26/08/2026',
|
||||||
|
'nav.privacy': 'Política de privacidad',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default es;
|
export default es;
|
||||||
|
|||||||
Reference in New Issue
Block a user