From 4e880c7510a75b29d556bea33dbf57ed75409cae Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:10:37 +0100 Subject: [PATCH] Policies: summon the guest sign-up banner when a guest clicks a policy (#6644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guests (anonymous users on a login-enabled deployment) could open a policy's setup/detail. Policies are an account feature, so a guest clicking a policy should be nudged to sign up rather than opening it. ## Behaviour A guest clicking a policy row — or a collapsed-rail icon — now **re-summons the existing guest sign-up banner** ("You're using Stirling PDF as a guest!…") and does **not** open the policy. - `GuestUserBanner` listens for a `stirling:show-guest-banner` window event and re-shows (even if previously dismissed; the render guard still hides it for non-anonymous users). - The policy sidebar dispatches that event on a guest click (cross-layer via `CustomEvent`, same pattern as `payg:signupRequired`; a no-op on builds without the banner). - `usePolicyGuestBlocked()` gates it: `config.enableLogin === true && user.is_anonymous === true`. - **Login-disabled single-user** deployments have an anonymous local operator with full access → not gated. ## Verification - Typecheck clean (proprietary + saas); eslint clean; sidebar tests pass. ## Note No dedicated guest unit test — the suite mocks `useAppConfig` at module scope, and making it per-test controllable needs `vi.hoisted` plumbing that risked the existing tests. Easy follow-up. --- .../components/policies/PoliciesSidebar.tsx | 31 ++++++++++++++++++- .../saas/components/auth/GuestUserBanner.tsx | 13 ++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx index f0c4fb959..8b6e551cd 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx @@ -18,6 +18,8 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import LocalIcon from "@app/components/shared/LocalIcon"; import { usePolicies } from "@app/hooks/usePolicies"; import { usePolicyCatalog } from "@app/hooks/usePolicyCatalog"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useAuth } from "@app/auth/UseSession"; import { getPolicyAutomation } from "@app/services/policyFolders"; import { watchedFolderStorage } from "@app/services/watchedFolderStorage"; import { runsToActivity, runsToStats } from "@app/services/policyLiveData"; @@ -59,6 +61,25 @@ export function usePoliciesEnabled(): boolean { return POLICIES_ENABLED; } +/** + * Whether the current user is a guest who can't open or configure policies — + * an anonymous user on a login-enabled deployment (i.e. a SaaS sign-up prompt + * candidate). The policy list stays visible but its rows don't open; the guest + * sign-up banner explains why. A login-disabled single-user deployment has an + * anonymous local operator with full access, so it is not gated. + */ +export function usePolicyGuestBlocked(): boolean { + const { config } = useAppConfig(); + const { user } = useAuth(); + return config?.enableLogin === true && user?.is_anonymous === true; +} + +/** Re-summon the guest sign-up banner (the saas GuestUserBanner listens for this; + * a no-op on builds without it). Used when a guest clicks a gated policy. */ +function promptGuestSignup(): void { + window.dispatchEvent(new CustomEvent("stirling:show-guest-banner")); +} + /** * Whether a policy is open — i.e. its detail view should take over the rail in * place of the tool list. False when the feature is off or nothing is selected. @@ -79,6 +100,7 @@ export function PoliciesSection({ const { t } = useTranslation(); const pol = usePolicies(); const { categories } = usePolicyCatalog(); + const guestBlocked = usePolicyGuestBlocked(); // Persist the expand/collapse state across refreshes. const [expanded, setExpanded] = useState(() => { try { @@ -179,7 +201,9 @@ export function PoliciesSection({ key={cat.id} type="button" className="pol-row" - onClick={() => selectPolicy(cat.id)} + onClick={() => + guestBlocked ? promptGuestSignup() : selectPolicy(cat.id) + } > {cat.icon} @@ -429,6 +453,7 @@ export function PoliciesCollapsedButton({ const { t } = useTranslation(); const pol = usePolicies(); const { categories } = usePolicyCatalog(); + const guestBlocked = usePolicyGuestBlocked(); if (!POLICIES_ENABLED) return null; @@ -468,6 +493,10 @@ export function PoliciesCollapsedButton({ { label, status: statusLabel }, )} onClick={() => { + if (guestBlocked) { + promptGuestSignup(); + return; + } selectPolicy(cat.id); onExpand(); }} diff --git a/frontend/editor/src/saas/components/auth/GuestUserBanner.tsx b/frontend/editor/src/saas/components/auth/GuestUserBanner.tsx index 3550042b7..160b0c56a 100644 --- a/frontend/editor/src/saas/components/auth/GuestUserBanner.tsx +++ b/frontend/editor/src/saas/components/auth/GuestUserBanner.tsx @@ -37,6 +37,19 @@ export function GuestUserBanner({ className = "" }: GuestUserBannerProps) { return () => clearTimeout(timer); }, [isAnon]); + // Let other surfaces re-summon the banner on demand (e.g. a guest clicking a + // gated feature). Re-show even if previously dismissed; the render guard below + // still hides it for non-anonymous users. + useEffect(() => { + const show = () => { + setIsDismissed(false); + setVisible(true); + hasShownThisLoad = true; + }; + window.addEventListener("stirling:show-guest-banner", show); + return () => window.removeEventListener("stirling:show-guest-banner", show); + }, []); + if (!isAnon || isDismissed || !visible) { return null; }