feat(mobile): add complete consent flow (banner, health modal, privacy, i18n)
This commit is contained in:
@@ -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 */}
|
||||
|
||||
@@ -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'} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user