feat(license): simplify community license page to activate form + pricing link (#892)

Replace the in-app upgrade promotion (Try Admiral free callout, Skipper
and Admiral upgrade cards with feature lists, monthly/annual checkout
buttons) with a single subdued "See pricing" link to sencho.io/pricing.

The marketing site carries the upgrade story now; the dashboard's job
is to show the operator their current plan, accept a license key when
they have one, and point them to one place if they want to learn more.

Behavior by tier:
- Community: Plan info, Activate input, "See pricing" link.
- Trial (paid): Plan info, Activate input, no pricing link (the user
  already received a key by email).
- Active paid: Plan info with customer/product/key, Manage subscription
  and Deactivate. Unchanged from before.
- Expired paid: Plan info, Activate input, "See pricing" link as a
  recovery path.

Drops the SKIPPER / ADMIRAL_MONTHLY / ADMIRAL_ANNUAL Lemon Squeezy
checkout URL constants and the inline UpgradeCard component. -129 LOC
net.
This commit is contained in:
Anso
2026-05-03 01:31:00 -04:00
committed by GitHub
parent 1f8ce773ff
commit c06f937d8d
@@ -5,22 +5,16 @@ import { toast } from '@/components/ui/toast-store';
import { useLicense } from '@/context/LicenseContext'; import { useLicense } from '@/context/LicenseContext';
import { TierBadge } from '@/components/TierBadge'; import { TierBadge } from '@/components/TierBadge';
import { import {
Crown, CheckCircle, Check, XCircle, Clock, ExternalLink, Crown, CheckCircle, XCircle, Clock, ExternalLink,
CreditCard, RefreshCw, Zap, Compass, ShipWheel, Loader2, CreditCard, RefreshCw, Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { SettingsSection } from './SettingsSection'; import { SettingsSection } from './SettingsSection';
import { SettingsField } from './SettingsField'; import { SettingsField } from './SettingsField';
import { SettingsCallout } from './SettingsCallout';
import { SettingsActions, SettingsPrimaryButton } from './SettingsActions'; import { SettingsActions, SettingsPrimaryButton } from './SettingsActions';
import { useMastheadStats } from './MastheadStatsContext'; import { useMastheadStats } from './MastheadStatsContext';
// Lemon Squeezy hosted checkout URLs. Admiral monthly and annual include a built-in const PRICING_URL = 'https://sencho.io/pricing';
// 14-day trial (email + card required on LS checkout); users receive a license key
// by email and paste it into the activate input below.
const SKIPPER_CHECKOUT_URL = 'https://saelix.lemonsqueezy.com/checkout/buy/f75bfb65-443a-46a0-abb1-981e0ff4b382';
const ADMIRAL_MONTHLY_CHECKOUT_URL = 'https://saelix.lemonsqueezy.com/checkout/buy/b049b824-176a-408d-a9d3-9365c979a61f';
const ADMIRAL_ANNUAL_CHECKOUT_URL = 'https://saelix.lemonsqueezy.com/checkout/buy/3e595568-92d3-4edd-90f1-25650248dfa9';
function getTierDisplayName(tier?: string, variant?: string | null, status?: string): string { function getTierDisplayName(tier?: string, variant?: string | null, status?: string): string {
if (tier === 'paid' && variant === 'admiral' && status === 'active') return 'Sencho Admiral'; if (tier === 'paid' && variant === 'admiral' && status === 'active') return 'Sencho Admiral';
@@ -59,11 +53,11 @@ export function LicenseSection() {
} }
}; };
const isAdmiral = isPaid && license?.variant === 'admiral' && license?.status === 'active'; // Pricing link covers two cases: Community-tier operators evaluating
const showSkipperCard = !isPaid || license?.status === 'trial'; // a paid plan, and expired paid licensees who need a path back. Active
const showTrialCta = license?.status !== 'active' && license?.status !== 'trial'; // and trial paid licensees already manage their plan through the
const showAdmiralUpgradeCard = !isAdmiral && !showTrialCta; // billing portal in the Plan section above.
const showUpgradeCards = showSkipperCard || showAdmiralUpgradeCard; const showPricingLink = !isPaid || license?.status === 'expired';
const renewsValue = useMemo(() => { const renewsValue = useMemo(() => {
if (!license) return null; if (!license) return null;
@@ -219,113 +213,6 @@ export function LicenseSection() {
) : null} ) : null}
</SettingsSection> </SettingsSection>
{showTrialCta ? (
<SettingsSection title="Try Admiral free">
<div className="pt-3 flex flex-col gap-3">
<SettingsCallout
tone="brand"
icon={<ShipWheel className="h-4 w-4" strokeWidth={1.5} />}
title="14 days, full Admiral"
subtitle="Host Console, Scheduled Operations, LDAP / Active Directory, audit log, API tokens, and unlimited accounts. Lemon Squeezy needs a card; cancel any time before day 14."
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<SettingsPrimaryButton
size="sm"
onClick={() => window.open(ADMIRAL_MONTHLY_CHECKOUT_URL, '_blank')}
>
Start monthly trial
<ExternalLink className="w-3 h-3 opacity-60" />
</SettingsPrimaryButton>
<Button
size="sm"
variant="outline"
onClick={() => window.open(ADMIRAL_ANNUAL_CHECKOUT_URL, '_blank')}
>
Start annual trial
<ExternalLink className="w-3 h-3 opacity-60" />
</Button>
</div>
<p className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle/70">
paste the license key from your email into the activate field below
</p>
</div>
</SettingsSection>
) : null}
{showUpgradeCards ? (
<SettingsSection title="Upgrade">
<div className={`pt-3 grid gap-3 ${showSkipperCard && showAdmiralUpgradeCard ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1'}`}>
{showSkipperCard ? (
<UpgradeCard
tone="warn"
icon={<Compass className="h-4 w-4" />}
title="Skipper"
blurb="Professional tools for solo operators."
features={[
'Fleet View with drill-down',
'Viewer accounts (1 admin + 3 viewers)',
'Webhooks & stack labels',
'Atomic deployments & backups',
'Auto-update policies',
'Google / GitHub / Okta SSO',
]}
action={
<SettingsPrimaryButton
size="sm"
className="w-full"
onClick={() => window.open(SKIPPER_CHECKOUT_URL, '_blank')}
>
<Zap className="w-4 h-4" />
Get Skipper
<ExternalLink className="w-3 h-3 opacity-60" />
</SettingsPrimaryButton>
}
/>
) : null}
{showAdmiralUpgradeCard ? (
<UpgradeCard
tone="brand"
icon={<ShipWheel className="h-4 w-4" />}
title="Admiral"
blurb="For teams managing shared infrastructure."
features={[
...(license?.variant === 'skipper' ? ['Everything in Skipper'] : ['Everything in Community']),
'Unlimited accounts & scoped RBAC',
...(license?.variant !== 'skipper' ? ['Fleet View, webhooks & labels', 'Atomic deployments & backups'] : []),
'LDAP/AD, audit log & host console',
'API tokens & private registries',
'Scheduled operations',
]}
action={
showSkipperCard ? (
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => window.open(ADMIRAL_MONTHLY_CHECKOUT_URL, '_blank')}
>
<Zap className="w-4 h-4" />
Get Admiral
<ExternalLink className="w-3 h-3 opacity-60" />
</Button>
) : (
<SettingsPrimaryButton
size="sm"
className="w-full"
onClick={() => window.open(ADMIRAL_MONTHLY_CHECKOUT_URL, '_blank')}
>
<Zap className="w-4 h-4" />
Get Admiral
<ExternalLink className="w-3 h-3 opacity-60" />
</SettingsPrimaryButton>
)
}
/>
) : null}
</div>
</SettingsSection>
) : null}
{license?.status !== 'active' ? ( {license?.status !== 'active' ? (
<SettingsSection title="Activate"> <SettingsSection title="Activate">
<SettingsField <SettingsField
@@ -369,37 +256,21 @@ export function LicenseSection() {
</SettingsField> </SettingsField>
</SettingsSection> </SettingsSection>
) : null} ) : null}
</div>
); {showPricingLink ? (
} <SettingsSection title="Pricing">
<div className="pt-[var(--density-row-y,0.75rem)]">
interface UpgradeCardProps { <Button
tone: 'warn' | 'brand'; variant="outline"
icon: React.ReactNode; size="sm"
title: string; onClick={() => window.open(PRICING_URL, '_blank')}
blurb: string; >
features: string[]; See pricing
action: React.ReactNode; <ExternalLink className="w-3 h-3 opacity-60" />
} </Button>
</div>
function UpgradeCard({ tone, icon, title, blurb, features, action }: UpgradeCardProps) { </SettingsSection>
const iconClass = tone === 'warn' ? 'text-warning' : 'text-brand'; ) : null}
return (
<div className="border border-card-border rounded-md bg-card p-4 flex flex-col gap-3">
<div className="flex items-center gap-2">
<span className={iconClass}>{icon}</span>
<span className="font-sans text-base text-stat-value">{title}</span>
</div>
<p className="text-xs text-stat-subtitle">{blurb}</p>
<ul className="space-y-1.5 flex-1">
{features.map((f) => (
<li key={f} className="flex items-start gap-2 text-xs text-stat-subtitle">
<Check className="h-3 w-3 shrink-0 text-brand mt-0.5" />
{f}
</li>
))}
</ul>
<div className="mt-auto">{action}</div>
</div> </div>
); );
} }