diff --git a/apps/frontend/src/App.jsx b/apps/frontend/src/App.jsx index b2153f1..f3e7b29 100644 --- a/apps/frontend/src/App.jsx +++ b/apps/frontend/src/App.jsx @@ -12,7 +12,11 @@ import ForgotPasswordModal from './components/ForgotPasswordModal'; import ResetPasswordView from './views/ResetPasswordView'; import SavedNotifications from './components/SavedNotifications'; import BottomNav from './components/BottomNav'; +import CookieBanner from './components/CookieBanner'; +import HealthConsentModal from './components/HealthConsentModal'; +import PrivacyView from './views/PrivacyView'; import { getFaro } from './utils/faro'; +import { hasConsentChoice, getLocalConsents, saveConsents, fetchServerConsents } from './utils/consent'; function App() { const [screen, setScreen] = useState('home'); @@ -26,6 +30,10 @@ function App() { const [badgeCount, setBadgeCount] = useState(0); const [prescriptionSearch, setPrescriptionSearch] = useState(''); const [productScreen, setProductScreen] = useState(null); + const [showCookieBanner, setShowCookieBanner] = useState(!hasConsentChoice()); + const [showHealthConsent, setShowHealthConsent] = useState(false); + const [pendingTsiScan, setPendingTsiScan] = useState(null); + const [consents, setConsents] = useState(getLocalConsents); const [screenSize, setScreenSize] = useState({ width: window.innerWidth, height: window.innerHeight @@ -109,6 +117,14 @@ function App() { return () => { cancelled = true; }; }, [currentUser]); + useEffect(() => { + if (currentUser) { + fetchServerConsents().then(serverConsents => { + if (serverConsents) setConsents(serverConsents); + }); + } + }, [currentUser]); + function refreshBadgeCount() { if (!currentUser) return; fetch('/api/notifications/mine', { credentials: 'include' }) @@ -150,6 +166,36 @@ function App() { window.history.replaceState({}, '', '/'); } + async function handleCookieConsent(newConsents) { + const saved = await saveConsents(newConsents); + setConsents(saved); + setShowCookieBanner(false); + } + + function handleTsiScanRequest(scanFn) { + if (consents.health_data) { + scanFn(); + } else { + setPendingTsiScan(() => scanFn); + setShowHealthConsent(true); + } + } + + function handleHealthConsentAccept() { + saveConsents({ ...consents, health_data: true }); + setConsents(prev => ({ ...prev, health_data: true })); + setShowHealthConsent(false); + if (pendingTsiScan) { + pendingTsiScan(); + setPendingTsiScan(null); + } + } + + function handleHealthConsentCancel() { + setShowHealthConsent(false); + setPendingTsiScan(null); + } + function handleAdminClick() { setScreen('admin'); } @@ -170,6 +216,7 @@ function App() { else setShowLogin(true); return; } + if (tab === 'privacy') { setScreen('privacy'); return; } } let activeView; @@ -232,6 +279,8 @@ function App() { setPrescriptionSearch(name); setScreen('search'); }} + onTsiScanRequest={handleTsiScanRequest} + consents={consents} /> ); break; @@ -246,6 +295,9 @@ function App() { case 'admin': activeView = ; break; + case 'privacy': + activeView = setScreen('home')} />; + break; default: activeView = ( setShowSaved(false)} onNotificationChange={refreshBadgeCount} /> )} + + {showCookieBanner && ( + { setShowCookieBanner(false); setScreen('privacy'); }} + /> + )} + {showHealthConsent && ( + + )} ); } diff --git a/apps/frontend/src/components/BottomNav.css b/apps/frontend/src/components/BottomNav.css index 4b28750..8c4adf8 100644 --- a/apps/frontend/src/components/BottomNav.css +++ b/apps/frontend/src/components/BottomNav.css @@ -141,3 +141,6 @@ .nav-elevated.active .nav-label { color: var(--tertiary); } + +.bottom-nav-privacy { display: block; width: 100%; text-align: center; padding: 0.35rem 0 0; background: none; border: none; color: var(--text-muted); font-size: 0.7rem; cursor: pointer; text-decoration: underline; } +.bottom-nav-privacy:hover { color: var(--text-main); } diff --git a/apps/frontend/src/components/BottomNav.jsx b/apps/frontend/src/components/BottomNav.jsx index 7d172d5..babbbd6 100644 --- a/apps/frontend/src/components/BottomNav.jsx +++ b/apps/frontend/src/components/BottomNav.jsx @@ -53,6 +53,9 @@ function BottomNav({ activeTab, onChange, isLoggedIn, badgeCount }) { ); })} + ); } diff --git a/apps/frontend/src/main.jsx b/apps/frontend/src/main.jsx index 34bce58..eab55f8 100644 --- a/apps/frontend/src/main.jsx +++ b/apps/frontend/src/main.jsx @@ -9,7 +9,12 @@ import { initFaro } from './utils/faro'; // Initialize Grafana Faro (browser RUM) before rendering. // No-op if VITE_FARO_ENDPOINT is not configured. -initFaro(); +try { + const consents = JSON.parse(localStorage.getItem('farmafinder_consents') || '{}'); + if (consents.analytics) { + initFaro(); + } +} catch {} // Global unhandled promise rejection handler — report to Faro window.addEventListener('unhandledrejection', (event) => { diff --git a/apps/frontend/src/views/ScannerView.jsx b/apps/frontend/src/views/ScannerView.jsx index dc23314..3ccd78f 100644 --- a/apps/frontend/src/views/ScannerView.jsx +++ b/apps/frontend/src/views/ScannerView.jsx @@ -25,7 +25,7 @@ function playBeep() { } catch (_) { } } -function ScannerView({ onClose, onSelectMedicine }) { +function ScannerView({ onClose, onSelectMedicine, onTsiScanRequest, consents }) { const { t } = useTranslation(); const videoRef = useRef(null); const streamRef = useRef(null); @@ -219,10 +219,13 @@ function ScannerView({ onClose, onSelectMedicine }) { } function handleStartScan() { - if (isNative) { - handleNativeScan(); + const doScan = () => { + if (isNative) { handleNativeScan(); } else { handleWebScan(); } + }; + if (onTsiScanRequest) { + onTsiScanRequest(doScan); } else { - handleWebScan(); + doScan(); } }