mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Policies: summon the guest sign-up banner when a guest clicks a policy (#6644)
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.
This commit is contained in:
@@ -18,6 +18,8 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|||||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||||
import { usePolicies } from "@app/hooks/usePolicies";
|
import { usePolicies } from "@app/hooks/usePolicies";
|
||||||
import { usePolicyCatalog } from "@app/hooks/usePolicyCatalog";
|
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 { getPolicyAutomation } from "@app/services/policyFolders";
|
||||||
import { watchedFolderStorage } from "@app/services/watchedFolderStorage";
|
import { watchedFolderStorage } from "@app/services/watchedFolderStorage";
|
||||||
import { runsToActivity, runsToStats } from "@app/services/policyLiveData";
|
import { runsToActivity, runsToStats } from "@app/services/policyLiveData";
|
||||||
@@ -59,6 +61,25 @@ export function usePoliciesEnabled(): boolean {
|
|||||||
return POLICIES_ENABLED;
|
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
|
* 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.
|
* 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 { t } = useTranslation();
|
||||||
const pol = usePolicies();
|
const pol = usePolicies();
|
||||||
const { categories } = usePolicyCatalog();
|
const { categories } = usePolicyCatalog();
|
||||||
|
const guestBlocked = usePolicyGuestBlocked();
|
||||||
// Persist the expand/collapse state across refreshes.
|
// Persist the expand/collapse state across refreshes.
|
||||||
const [expanded, setExpanded] = useState(() => {
|
const [expanded, setExpanded] = useState(() => {
|
||||||
try {
|
try {
|
||||||
@@ -179,7 +201,9 @@ export function PoliciesSection({
|
|||||||
key={cat.id}
|
key={cat.id}
|
||||||
type="button"
|
type="button"
|
||||||
className="pol-row"
|
className="pol-row"
|
||||||
onClick={() => selectPolicy(cat.id)}
|
onClick={() =>
|
||||||
|
guestBlocked ? promptGuestSignup() : selectPolicy(cat.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
||||||
{cat.icon}
|
{cat.icon}
|
||||||
@@ -429,6 +453,7 @@ export function PoliciesCollapsedButton({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const pol = usePolicies();
|
const pol = usePolicies();
|
||||||
const { categories } = usePolicyCatalog();
|
const { categories } = usePolicyCatalog();
|
||||||
|
const guestBlocked = usePolicyGuestBlocked();
|
||||||
|
|
||||||
if (!POLICIES_ENABLED) return null;
|
if (!POLICIES_ENABLED) return null;
|
||||||
|
|
||||||
@@ -468,6 +493,10 @@ export function PoliciesCollapsedButton({
|
|||||||
{ label, status: statusLabel },
|
{ label, status: statusLabel },
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (guestBlocked) {
|
||||||
|
promptGuestSignup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
selectPolicy(cat.id);
|
selectPolicy(cat.id);
|
||||||
onExpand();
|
onExpand();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ export function GuestUserBanner({ className = "" }: GuestUserBannerProps) {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isAnon]);
|
}, [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) {
|
if (!isAnon || isDismissed || !visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user