From 65d25e3ce5e481e4e8a4b4aedb5516a8bf5e0f20 Mon Sep 17 00:00:00 2001 From: Richard R Date: Sat, 23 May 2026 05:50:54 -0600 Subject: [PATCH] refactor(ui): centralize privacy gating logic for settings modal Consolidate privacy acceptance checks into reusable hooks and callbacks within SettingsModal. Replace scattered gating with a single entry point for opening settings or changelog views, ensuring consistent enforcement. Update test helpers to robustly dismiss onboarding and settings overlays by avoiding state race conditions. --- src/components/SettingsModal.tsx | 54 +++++++++++++++++++++++++++----- tests/helpers.ts | 47 +++++++++++++-------------- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index ec1d72d..3a25446 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -208,9 +208,32 @@ export function SettingsModal({ className = '' }: { className?: string }) { const isSharedSelected = Boolean(selectedSharedProvider); const selectedProviderOption = ttsProviders.find((p) => p.id === localProviderRef) ?? ttsProviders[0]; - const checkFirstVist = useCallback(async () => { + const closeSettingsForPrivacyGate = useCallback(() => { + setIsOpen(false); + setIsChangelogOpen(false); + }, []); + + const canOpenSettings = useCallback(async () => { + if (!authEnabled) { + return true; + } const appConfig = await getAppConfig(); - if (authEnabled && !appConfig?.privacyAccepted) { + return Boolean(appConfig?.privacyAccepted); + }, [authEnabled]); + + const openSettings = useCallback(async (options?: { changelog?: boolean }) => { + const allowed = await canOpenSettings(); + if (!allowed) { + closeSettingsForPrivacyGate(); + return; + } + setIsOpen(true); + setIsChangelogOpen(Boolean(options?.changelog)); + }, [canOpenSettings, closeSettingsForPrivacyGate]); + + const checkFirstVist = useCallback(async () => { + const allowed = await canOpenSettings(); + if (!allowed) { return; } const firstVisit = await getFirstVisit(); @@ -218,7 +241,7 @@ export function SettingsModal({ className = '' }: { className?: string }) { await setFirstVisit(true); setIsOpen(true); } - }, [authEnabled, setIsOpen]); + }, [canOpenSettings, setIsOpen]); useEffect(() => { checkFirstVist().catch((err) => { @@ -250,6 +273,22 @@ export function SettingsModal({ className = '' }: { className?: string }) { }; }, [authEnabled, checkFirstVist]); + useEffect(() => { + if (!isOpen) { + return; + } + let cancelled = false; + void (async () => { + const allowed = await canOpenSettings(); + if (!allowed && !cancelled) { + closeSettingsForPrivacyGate(); + } + })(); + return () => { + cancelled = true; + }; + }, [isOpen, canOpenSettings, closeSettingsForPrivacyGate]); + useEffect(() => { return scheduleChangelogCheck({ authEnabled, @@ -260,13 +299,12 @@ export function SettingsModal({ className = '' }: { className?: string }) { inFlightRef: changelogVersionCheckInFlightRef, postCheck: async (currentVersion) => postChangelogVersionCheck(currentVersion), onShouldOpen: () => { - setIsOpen(true); - setIsChangelogOpen(true); + void openSettings({ changelog: true }); }, delayMs: 120, retryDelayMs: 400, }); - }, [authEnabled, isSessionPending, session?.user?.id, runtimeConfig.appVersion]); + }, [authEnabled, isSessionPending, session?.user?.id, runtimeConfig.appVersion, openSettings]); useEffect(() => { if (!ttsModels.some(m => m.id === modelValue) && modelValue !== '') { @@ -532,7 +570,9 @@ export function SettingsModal({ className = '' }: { className?: string }) { return ( <>