fix(licensing): rename variant values to skipper/admiral and store resolved type (#379)

Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral',
aligning code with user-facing tier names. Variant type is now resolved once
at activation/validation and stored in DB via license_variant_type, instead
of string-matching the Lemon Squeezy variant_name on every read. Also captures
variant_id for future lookups. Pre-existing installs auto-migrate on first
getVariant() call.
This commit is contained in:
Anso
2026-04-05 18:45:57 -04:00
committed by GitHub
parent 4163af2ee7
commit 797623e56f
17 changed files with 126 additions and 78 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ export function AdmiralGate({ children, featureName = 'This feature' }: AdmiralG
const { isPaid, license } = useLicense();
const [dismissed, setDismissed] = useState(isDismissedFromStorage);
if (isPaid && license?.variant === 'team') return <>{children}</>;
if (isPaid && license?.variant === 'admiral') return <>{children}</>;
if (dismissed) {
return (
+1 -1
View File
@@ -223,7 +223,7 @@ export default function EditorLayout() {
if (isPaid && isAdmin) {
items.push({ value: 'auto-updates', label: 'Auto-Update', icon: RefreshCw });
}
if (isPaid && license?.variant === 'team') {
if (isPaid && license?.variant === 'admiral') {
if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal });
if (can('system:audit')) items.push({ value: 'audit-log', label: 'Audit', icon: ScrollText });
if (isAdmin) items.push({ value: 'scheduled-ops', label: 'Schedules', icon: Clock });
+1 -1
View File
@@ -303,7 +303,7 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal
}
};
const isAdmiral = isPaid && license?.variant === 'team';
const isAdmiral = isPaid && license?.variant === 'admiral';
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
+5 -6
View File
@@ -11,15 +11,14 @@ interface TierBadgeProps {
const tierConfig = {
community: { icon: Globe, label: 'Community' },
paid: { icon: Compass, label: 'Skipper' },
team: { icon: ShipWheel, label: 'Admiral' },
skipper: { icon: Compass, label: 'Skipper' },
admiral: { icon: ShipWheel, label: 'Admiral' },
} as const;
function resolveTier(tier: LicenseTier, variant: LicenseVariant, status: LicenseStatus) {
// Only show Team badge for active team licenses, not trials
// (trials default to team variant to unlock all features)
if (tier === 'paid' && variant === 'team' && status === 'active') return tierConfig.team;
if (tier === 'paid') return tierConfig.paid;
// Only show Admiral badge for active admiral licenses (trials default to skipper)
if (tier === 'paid' && variant === 'admiral' && status === 'active') return tierConfig.admiral;
if (tier === 'paid') return tierConfig.skipper;
return tierConfig.community;
}
@@ -143,7 +143,7 @@ export function DeveloperSection({ settings, onSettingChange, onSave, isSaving,
</div>
</div>
{isPaid && license?.variant === 'team' && (
{isPaid && license?.variant === 'admiral' && (
<div className="flex items-center justify-between gap-4 pt-4 border-t border-glass-border">
<div className="space-y-0.5">
<Label className="text-base">Audit Log Retention</Label>
@@ -13,7 +13,7 @@ import {
import { apiFetch } from '@/lib/api';
function getTierDisplayName(tier?: string, variant?: string | null, status?: string): string {
if (tier === 'paid' && variant === 'team' && status === 'active') return 'Sencho Admiral';
if (tier === 'paid' && variant === 'admiral' && status === 'active') return 'Sencho Admiral';
if (tier === 'paid') return 'Sencho Skipper';
return 'Sencho Community';
}
@@ -42,9 +42,9 @@ export function LicenseSection() {
}
};
const isAdmiral = isPaid && license?.variant === 'team' && license?.status === 'active';
const isAdmiral = isPaid && license?.variant === 'admiral' && license?.status === 'active';
const showSkipperCard = !isPaid || license?.status === 'trial';
const showUpgradeCards = !isAdmiral && (showSkipperCard || (license?.variant === 'personal' && license?.status === 'active'));
const showUpgradeCards = !isAdmiral && (showSkipperCard || (license?.variant === 'skipper' && license?.status === 'active'));
return (
<div className="space-y-6">
@@ -210,9 +210,9 @@ export function LicenseSection() {
<p className="text-xs text-muted-foreground">For teams managing shared infrastructure.</p>
<ul className="space-y-1.5">
{[
...(license?.variant === 'personal' ? ['Everything in Skipper'] : ['Everything in Community']),
...(license?.variant === 'skipper' ? ['Everything in Skipper'] : ['Everything in Community']),
'Unlimited accounts & scoped RBAC',
...(license?.variant !== 'personal' ? ['Fleet View, webhooks & labels', 'Atomic deployments & backups'] : []),
...(license?.variant !== 'skipper' ? ['Fleet View, webhooks & labels', 'Atomic deployments & backups'] : []),
'SSO, audit log & host console',
'API tokens & private registries',
'Scheduled operations',
@@ -49,17 +49,17 @@ export function SupportSection() {
Priority Support <TierBadge />
</h4>
<div className="grid gap-3">
<a href={license?.variant === 'team' ? 'mailto:support@sencho.io' : 'mailto:licensing@sencho.io'}
<a href={license?.variant === 'admiral' ? 'mailto:support@sencho.io' : 'mailto:licensing@sencho.io'}
className="flex items-center gap-3 p-3 rounded-lg border border-glass-border hover:bg-muted/50 transition-colors">
<div className="w-9 h-9 rounded-lg bg-muted flex items-center justify-center shrink-0">
<Mail className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">
{license?.variant === 'team' ? 'Priority Email Support' : 'Email Support'}
{license?.variant === 'admiral' ? 'Priority Email Support' : 'Email Support'}
</p>
<p className="text-xs text-muted-foreground">
{license?.variant === 'team'
{license?.variant === 'admiral'
? 'Direct support with responses within 24 hours'
: 'Reach our support team directly'}
</p>
@@ -267,7 +267,7 @@ export function UsersSection() {
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
{isPaid && license?.variant === 'team' && (
{isPaid && license?.variant === 'admiral' && (
<>
<SelectItem value="deployer">Deployer</SelectItem>
<SelectItem value="node-admin">Node Admin</SelectItem>
@@ -306,7 +306,7 @@ export function UsersSection() {
</div>
{/* Scoped Permissions (Admiral, editing only) */}
{editingUser && isPaid && license?.variant === 'team' && (
{editingUser && isPaid && license?.variant === 'admiral' && (
<div className="border border-glass-border rounded-lg p-4 space-y-3 mt-4">
<h4 className="text-sm font-medium">Scoped Permissions</h4>
<p className="text-xs text-muted-foreground">