Privacy settings #60
@@ -12,7 +12,11 @@ import ForgotPasswordModal from './components/ForgotPasswordModal';
|
|||||||
import ResetPasswordView from './views/ResetPasswordView';
|
import ResetPasswordView from './views/ResetPasswordView';
|
||||||
import SavedNotifications from './components/SavedNotifications';
|
import SavedNotifications from './components/SavedNotifications';
|
||||||
import BottomNav from './components/BottomNav';
|
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 { getFaro } from './utils/faro';
|
||||||
|
import { hasConsentChoice, getLocalConsents, saveConsents, fetchServerConsents } from './utils/consent';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [screen, setScreen] = useState('home');
|
const [screen, setScreen] = useState('home');
|
||||||
@@ -26,6 +30,10 @@ function App() {
|
|||||||
const [badgeCount, setBadgeCount] = useState(0);
|
const [badgeCount, setBadgeCount] = useState(0);
|
||||||
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
||||||
const [productScreen, setProductScreen] = useState(null);
|
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({
|
const [screenSize, setScreenSize] = useState({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight
|
height: window.innerHeight
|
||||||
@@ -109,6 +117,14 @@ function App() {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser) {
|
||||||
|
fetchServerConsents().then(serverConsents => {
|
||||||
|
if (serverConsents) setConsents(serverConsents);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentUser]);
|
||||||
|
|
||||||
function refreshBadgeCount() {
|
function refreshBadgeCount() {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
fetch('/api/notifications/mine', { credentials: 'include' })
|
fetch('/api/notifications/mine', { credentials: 'include' })
|
||||||
@@ -150,6 +166,36 @@ function App() {
|
|||||||
window.history.replaceState({}, '', '/');
|
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() {
|
function handleAdminClick() {
|
||||||
setScreen('admin');
|
setScreen('admin');
|
||||||
}
|
}
|
||||||
@@ -170,6 +216,7 @@ function App() {
|
|||||||
else setShowLogin(true);
|
else setShowLogin(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (tab === 'privacy') { setScreen('privacy'); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeView;
|
let activeView;
|
||||||
@@ -232,6 +279,8 @@ function App() {
|
|||||||
setPrescriptionSearch(name);
|
setPrescriptionSearch(name);
|
||||||
setScreen('search');
|
setScreen('search');
|
||||||
}}
|
}}
|
||||||
|
onTsiScanRequest={handleTsiScanRequest}
|
||||||
|
consents={consents}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -246,6 +295,9 @@ function App() {
|
|||||||
case 'admin':
|
case 'admin':
|
||||||
activeView = <AdminView />;
|
activeView = <AdminView />;
|
||||||
break;
|
break;
|
||||||
|
case 'privacy':
|
||||||
|
activeView = <PrivacyView onBack={() => setScreen('home')} />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
activeView = (
|
activeView = (
|
||||||
<HomeView
|
<HomeView
|
||||||
@@ -293,6 +345,16 @@ function App() {
|
|||||||
{showSaved && currentUser && (
|
{showSaved && currentUser && (
|
||||||
<SavedNotifications onClose={() => setShowSaved(false)} onNotificationChange={refreshBadgeCount} />
|
<SavedNotifications onClose={() => setShowSaved(false)} onNotificationChange={refreshBadgeCount} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showCookieBanner && (
|
||||||
|
<CookieBanner
|
||||||
|
onConsent={handleCookieConsent}
|
||||||
|
onPrivacyClick={() => { setShowCookieBanner(false); setScreen('privacy'); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showHealthConsent && (
|
||||||
|
<HealthConsentModal onAccept={handleHealthConsentAccept} onCancel={handleHealthConsentCancel} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,3 +141,6 @@
|
|||||||
.nav-elevated.active .nav-label {
|
.nav-elevated.active .nav-label {
|
||||||
color: var(--tertiary);
|
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); }
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ function BottomNav({ activeTab, onChange, isLoggedIn, badgeCount }) {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<button type="button" className="bottom-nav-privacy" onClick={() => onChange('privacy')}>
|
||||||
|
{t('nav.privacy')}
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import { initFaro } from './utils/faro';
|
|||||||
|
|
||||||
// Initialize Grafana Faro (browser RUM) before rendering.
|
// Initialize Grafana Faro (browser RUM) before rendering.
|
||||||
// No-op if VITE_FARO_ENDPOINT is not configured.
|
// No-op if VITE_FARO_ENDPOINT is not configured.
|
||||||
|
try {
|
||||||
|
const consents = JSON.parse(localStorage.getItem('farmafinder_consents') || '{}');
|
||||||
|
if (consents.analytics) {
|
||||||
initFaro();
|
initFaro();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// Global unhandled promise rejection handler — report to Faro
|
// Global unhandled promise rejection handler — report to Faro
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function playBeep() {
|
|||||||
} catch (_) { }
|
} catch (_) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScannerView({ onClose, onSelectMedicine }) {
|
function ScannerView({ onClose, onSelectMedicine, onTsiScanRequest, consents }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const streamRef = useRef(null);
|
const streamRef = useRef(null);
|
||||||
@@ -219,10 +219,13 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleStartScan() {
|
function handleStartScan() {
|
||||||
if (isNative) {
|
const doScan = () => {
|
||||||
handleNativeScan();
|
if (isNative) { handleNativeScan(); } else { handleWebScan(); }
|
||||||
|
};
|
||||||
|
if (onTsiScanRequest) {
|
||||||
|
onTsiScanRequest(doScan);
|
||||||
} else {
|
} else {
|
||||||
handleWebScan();
|
doScan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user