feat(ui): hide paid features from community-tier dashboard (#891)

* feat(ui): hide paid features from community-tier dashboard

Community installs render only the features they can use. Tier-locked
sections, lock badges, upsell cards, and "Upgrade" buttons no longer
appear anywhere except the License page in Settings, which is the
single discoverable upgrade path.

Concretely:

- PaidGate and AdmiralGate now render null for non-qualifying tiers
  instead of upsell cards.
- SectionGate (settings) hides tier-locked sections entirely.
- Settings sidebar and command palette filter out items the operator
  cannot reach.
- Configuration Status widget on the dashboard drops the Automation
  section for community and hides any locked rows in remaining
  sections.
- Fleet > Status node cards drop locked summary rows.
- Stack action menu, sidebar bulk bar, file upload / download, scan
  comparison, network topology toggle, node label picker all hide
  for community instead of showing disabled affordances or "Upgrade"
  literal text.
- Removes tierUpsell, TierLockChip, and useDismissalState (no longer
  referenced).

Backend tier guards remain authoritative; this changes UI discovery
only.

* test(e2e): assert upload control is absent in community tier

The community-clean-ui change removes the "Upgrade to unlock upload"
pill from the file explorer. Update the matching e2e assertion to
verify the upload control is not rendered, instead of waiting for a
pill that no longer exists.
This commit is contained in:
Anso
2026-05-03 01:17:02 -04:00
committed by GitHub
parent b9ada7f50b
commit 1f8ce773ff
34 changed files with 196 additions and 806 deletions
+11 -58
View File
@@ -1,65 +1,18 @@
import { Compass } from 'lucide-react';
import type { ReactNode } from 'react';
import { useLicense } from '@/context/LicenseContext';
import { useDismissalState } from '@/hooks/useDismissalState';
import {
CompactBlurredLock,
DismissedPill,
FullUpsellCard,
type TierGateProps,
} from './tierUpsell';
const DISMISS_KEY = 'sencho-upgrade-prompt-dismissed';
/**
* Gate for any paid-tier feature (Skipper or Admiral). Composes the
* shared tier-upsell primitives in `./tierUpsell` according to the
* current state:
* Thin wrapper that renders its children only for licensees on a paid
* plan (Skipper or Admiral). Community-tier users see nothing in this
* slot. Backend tier guards (`requirePaid`) remain the authoritative
* enforcement; this component only controls UI visibility.
*
* isPaid render children (unlocked).
* compact blurred children + small pill (inline list items).
* dismissed pill-only placeholder for 24h, click to restore.
* default full upsell card with View Plans CTA.
*
* The compact branch retains the blurred-children render because it is
* used for tiny inline UI where the visual continuity is intentional.
* The dismissed and default branches do not render children, so any
* lazy chunks behind the gate are never fetched on those paths.
* Use only when wrapping a discrete fragment that has no neighboring
* context for Community users. Where possible, prefer a parent-level
* `useLicense().isPaid` check that lifts the visibility decision out
* of the rendering tree entirely.
*/
export function PaidGate({ children, featureName = 'This feature', compact = false }: TierGateProps) {
export function PaidGate({ children }: { children: ReactNode }) {
const { isPaid } = useLicense();
const { dismissed, dismiss, restore } = useDismissalState(DISMISS_KEY);
if (isPaid) return <>{children}</>;
const pillText = `Upgrade to unlock ${featureName}`;
if (compact) {
return (
<CompactBlurredLock icon={Compass} pillText={pillText}>
{children}
</CompactBlurredLock>
);
}
if (dismissed) {
return <DismissedPill icon={Compass} pillText={pillText} onClick={restore} />;
}
return (
<FullUpsellCard
icon={Compass}
title={`${featureName} requires a paid license`}
body={
<>
Unlock features like fleet management, viewer accounts, one-click Google / GitHub / Okta SSO, and more with a Skipper or Admiral license.
For enterprise pricing or questions, contact{' '}
<a href="mailto:licensing@sencho.io" className="text-brand hover:underline">licensing@sencho.io</a>.
</>
}
ctaIcon={Compass}
ctaLabel="View Plans"
ctaHref="https://sencho.io/pricing"
onDismiss={dismiss}
/>
);
return isPaid ? <>{children}</> : null;
}