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 { 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)
|
||||
}
|
||||
>
|
||||
<IconBadge size="sm" accent={ROW_ACCENT[cat.id] ?? "blue"}>
|
||||
{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();
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user