From f115275796d74ac1b0a6ca3d4f31a0891c6ea610 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Wed, 22 Apr 2026 19:10:52 +0800 Subject: [PATCH] feat: add analytics consent page and auth flow integration --- apps/web/src/App.tsx | 20 +++- apps/web/src/hooks/use-auth.ts | 18 +++ apps/web/src/pages/analytics-consent-page.tsx | 107 ++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/pages/analytics-consent-page.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 397ff5ff..af6e007f 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -22,6 +22,9 @@ const LoginPage = lazy(() => import("./pages/login-page").then((m) => ({ default const PrivacyPolicyPage = lazy(() => import("./pages/privacy-policy-page").then((m) => ({ default: m.PrivacyPolicyPage })), ); +const AnalyticsConsentPage = lazy(() => + import("./pages/analytics-consent-page").then((m) => ({ default: m.AnalyticsConsentPage })), +); const ToolPage = lazy(() => import("./pages/tool-page").then((m) => ({ default: m.ToolPage }))); class ErrorBoundary extends Component< @@ -69,14 +72,22 @@ class ErrorBoundary extends Component< } function AuthGuard({ children }: { children: React.ReactNode }) { - const { loading, authEnabled, isAuthenticated, mustChangePassword } = useAuth(); + const { + loading, + authEnabled, + isAuthenticated, + mustChangePassword, + analyticsEnabled, + analyticsConsentShownAt, + } = useAuth(); const location = useLocation(); // Don't guard the login or change-password pages if ( location.pathname === "/login" || location.pathname === "/change-password" || - location.pathname === "/privacy" + location.pathname === "/privacy" || + location.pathname === "/analytics-consent" ) { return <>{children}; } @@ -101,6 +112,10 @@ function AuthGuard({ children }: { children: React.ReactNode }) { return ; } + if (authEnabled && analyticsEnabled === null && analyticsConsentShownAt === null) { + return ; + } + return <>{children}; } @@ -137,6 +152,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> diff --git a/apps/web/src/hooks/use-auth.ts b/apps/web/src/hooks/use-auth.ts index f69e78e3..89e1b5d8 100644 --- a/apps/web/src/hooks/use-auth.ts +++ b/apps/web/src/hooks/use-auth.ts @@ -9,6 +9,9 @@ interface AuthState { mustChangePassword: boolean; role: string | null; permissions: string[]; + analyticsEnabled: boolean | null; + analyticsConsentShownAt: number | null; + analyticsConsentRemindAt: number | null; } const USER_PERMISSIONS = [ @@ -27,6 +30,9 @@ export function useAuth() { mustChangePassword: false, role: null, permissions: [], + analyticsEnabled: null, + analyticsConsentShownAt: null, + analyticsConsentRemindAt: null, }); useEffect(() => { @@ -46,6 +52,9 @@ export function useAuth() { mustChangePassword: false, role: "user", permissions: USER_PERMISSIONS, + analyticsEnabled: null, + analyticsConsentShownAt: null, + analyticsConsentRemindAt: null, }); return; } @@ -60,6 +69,9 @@ export function useAuth() { mustChangePassword: false, role: null, permissions: [], + analyticsEnabled: null, + analyticsConsentShownAt: null, + analyticsConsentRemindAt: null, }); return; } @@ -79,6 +91,9 @@ export function useAuth() { mustChangePassword: mustChange, role: session.user?.role ?? null, permissions: session.user?.permissions ?? [], + analyticsEnabled: session.user?.analyticsEnabled ?? null, + analyticsConsentShownAt: session.user?.analyticsConsentShownAt ?? null, + analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null, }); } else { localStorage.removeItem("ashim-token"); @@ -90,6 +105,9 @@ export function useAuth() { mustChangePassword: false, role: null, permissions: [], + analyticsEnabled: null, + analyticsConsentShownAt: null, + analyticsConsentRemindAt: null, }); } } catch { diff --git a/apps/web/src/pages/analytics-consent-page.tsx b/apps/web/src/pages/analytics-consent-page.tsx new file mode 100644 index 00000000..7c00e2f3 --- /dev/null +++ b/apps/web/src/pages/analytics-consent-page.tsx @@ -0,0 +1,107 @@ +import { en } from "@ashim/shared"; +import { Shield } from "lucide-react"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAnalyticsStore } from "@/stores/analytics-store"; + +const t = en.analytics; + +export function AnalyticsConsentPage() { + const navigate = useNavigate(); + const { config, configLoaded, fetchConfig, acceptAnalytics, remindLater } = useAnalyticsStore(); + + useEffect(() => { + fetchConfig(); + }, [fetchConfig]); + + useEffect(() => { + if (configLoaded && !config?.enabled) { + navigate("/", { replace: true }); + } + }, [configLoaded, config, navigate]); + + if (!configLoaded) { + return ( +
+
+
+ ); + } + + const handleAccept = async () => { + await acceptAnalytics(); + navigate("/", { replace: true }); + }; + + const handleDecline = async () => { + await remindLater(); + navigate("/", { replace: true }); + }; + + return ( +
+
+
+
+ +
+
+ +
+

{t.consentTitle}

+

{t.consentDescription}

+
+ +

{t.consentPrivacy}

+ +
+
+

{t.whatShared}

+
    + {t.whatSharedItems.map((item) => ( +
  • + · + {item} +
  • + ))} +
+
+
+

{t.whatNever}

+
    + {t.whatNeverItems.map((item) => ( +
  • + · + {item} +
  • + ))} +
+
+
+ +

+ {t.consentProviders} +
+ {t.consentChangeable} +

+ +
+ + +
+
+
+ ); +}