refactor: rename Personal Pro to Skipper and Team Pro to Admiral (#256)

Align paid tier names with Sencho's nautical identity. Internal variant
values ('personal'/'team') remain unchanged in code, database, and
Lemon Squeezy integration — only user-facing display names updated.

- Backend: requireTeamPro → requireAdmiral, TEAM_PRO_REQUIRED → ADMIRAL_REQUIRED
- Frontend: TeamProGate.tsx → AdmiralGate.tsx, TierBadge labels updated
- Website: PricingSection tier names and nautical descriptions
- Docs: all 11 affected pages renamed, nautical footnote added to licensing
This commit is contained in:
Anso
2026-03-29 18:00:29 -04:00
committed by GitHub
parent 7838d6c32a
commit c328b7f49a
22 changed files with 120 additions and 116 deletions
@@ -3,12 +3,12 @@ import { Crown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLicense } from '@/context/LicenseContext';
interface TeamProGateProps {
interface AdmiralGateProps {
children: ReactNode;
featureName?: string;
}
const DISMISS_KEY = 'sencho-team-upgrade-prompt-dismissed';
const DISMISS_KEY = 'sencho-admiral-upgrade-prompt-dismissed';
const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
function isDismissedFromStorage(): boolean {
@@ -16,7 +16,7 @@ function isDismissedFromStorage(): boolean {
return !!dismissedAt && Date.now() - parseInt(dismissedAt, 10) < DISMISS_DURATION_MS;
}
export function TeamProGate({ children, featureName = 'This feature' }: TeamProGateProps) {
export function AdmiralGate({ children, featureName = 'This feature' }: AdmiralGateProps) {
const { isPro, license } = useLicense();
const [dismissed, setDismissed] = useState(isDismissedFromStorage);
@@ -31,7 +31,7 @@ export function TeamProGate({ children, featureName = 'This feature' }: TeamProG
<div className="absolute inset-0 flex items-start justify-center pt-8">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-muted/80 border border-border text-muted-foreground text-xs">
<Crown className="w-3 h-3" />
Upgrade to Team Pro to unlock
Upgrade to Admiral to unlock
</div>
</div>
</div>
@@ -44,9 +44,9 @@ export function TeamProGate({ children, featureName = 'This feature' }: TeamProG
<Crown className="w-8 h-8 text-muted-foreground" />
</div>
<div className="text-center max-w-md">
<h3 className="text-lg font-semibold mb-2">{featureName} requires Sencho Team Pro</h3>
<h3 className="text-lg font-semibold mb-2">{featureName} requires Sencho Admiral</h3>
<p className="text-sm text-muted-foreground">
Unlock team features like SSO authentication, audit logging, API tokens, and unlimited user accounts with a Sencho Team Pro license.
Unlock team features like SSO authentication, audit logging, API tokens, and unlimited user accounts with a Sencho Admiral license.
</p>
</div>
<div className="flex gap-3">
@@ -65,7 +65,7 @@ export function TeamProGate({ children, featureName = 'This feature' }: TeamProG
onClick={() => window.open('https://sencho.io/pricing', '_blank')}
>
<Crown className="w-4 h-4 mr-2" />
Get Team Pro
Get Admiral
</Button>
</div>
</div>
+3 -3
View File
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { TeamProGate } from './TeamProGate';
import { AdmiralGate } from './AdmiralGate';
import { TierBadge } from './TierBadge';
import { Zap, Plus, Copy, Trash2, CheckCircle, RefreshCw, Clock } from 'lucide-react';
@@ -123,7 +123,7 @@ export function ApiTokensSection() {
};
return (
<TeamProGate featureName="API Tokens">
<AdmiralGate featureName="API Tokens">
<div className="space-y-6">
<div className="flex items-start justify-between pr-8">
<div>
@@ -267,6 +267,6 @@ export function ApiTokensSection() {
</div>
))}
</div>
</TeamProGate>
</AdmiralGate>
);
}
@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { TeamProGate } from './TeamProGate';
import { AdmiralGate } from './AdmiralGate';
import { TierBadge } from './TierBadge';
import { Database, Plus, Trash2, Pencil, RefreshCw, CheckCircle, XCircle, Clock } from 'lucide-react';
@@ -205,7 +205,7 @@ export function RegistriesSection() {
};
return (
<TeamProGate featureName="Private Registry Management">
<AdmiralGate featureName="Private Registry Management">
<div className="space-y-6">
<div className="flex items-start justify-between pr-8">
<div>
@@ -379,6 +379,6 @@ export function RegistriesSection() {
</div>
))}
</div>
</TeamProGate>
</AdmiralGate>
);
}
+4 -4
View File
@@ -7,7 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { TeamProGate } from './TeamProGate';
import { AdmiralGate } from './AdmiralGate';
import { Shield, Loader2, CheckCircle, XCircle } from 'lucide-react';
interface SSOProviderConfig {
@@ -320,7 +320,7 @@ export function SSOSection() {
try {
const res = await apiFetch('/sso/config');
if (res.ok) setConfigs(await res.json());
} catch { /* ignore - TeamProGate will handle non-pro */ }
} catch { /* ignore - AdmiralGate will handle non-pro */ }
};
// eslint-disable-next-line react-hooks/set-state-in-effect
@@ -329,7 +329,7 @@ export function SSOSection() {
const getConfig = (provider: string) => configs.find(c => c.provider === provider) || null;
return (
<TeamProGate featureName="SSO Authentication">
<AdmiralGate featureName="SSO Authentication">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold tracking-tight flex items-center gap-2">
@@ -360,6 +360,6 @@ export function SSOSection() {
<p>For OIDC providers, set the OAuth callback URL to: <code className="bg-muted px-1 rounded">{'https://<your-sencho-url>/api/auth/sso/oidc/<provider>/callback'}</code></p>
</div>
</div>
</TeamProGate>
</AdmiralGate>
);
}
+9 -9
View File
@@ -663,7 +663,7 @@ function UsersSection() {
</Button>
</div>
{/* Scoped Permissions (Team Pro, editing only) */}
{/* Scoped Permissions (Admiral, editing only) */}
{editingUser && isPro && license?.variant === 'team' && (
<div className="border rounded-lg p-4 space-y-3 mt-4">
<h4 className="text-sm font-medium">Scoped Permissions</h4>
@@ -1365,17 +1365,17 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
</div>
)}
{/* Upgrade Cards - Community: show both, Personal Pro: show Team only, Team Pro: none */}
{/* Upgrade Cards - Community: show both, Skipper: show Admiral only, Admiral: none */}
{(license?.tier !== 'pro' || (license?.variant === 'personal' && license?.status === 'active')) && (
<div className="space-y-3">
<Label className="text-base">Upgrade your plan</Label>
<div className={`grid gap-3 ${license?.tier !== 'pro' ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1'}`}>
{/* Personal Pro Card - only for Community users */}
{/* Skipper Card - only for Community users */}
{license?.tier !== 'pro' && (
<div className="relative border border-border rounded-xl p-4 space-y-3 bg-muted/5">
<div className="flex items-center gap-2">
<Crown className="w-4 h-4 text-amber-500" />
<span className="font-semibold text-sm">Personal Pro</span>
<span className="font-semibold text-sm">Skipper</span>
<Badge variant="secondary" className="text-[10px] font-semibold uppercase px-1.5 py-0">Popular</Badge>
</div>
<p className="text-xs text-muted-foreground">Professional tools for solo operators.</p>
@@ -1393,22 +1393,22 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
onClick={() => window.open('https://saelix.lemonsqueezy.com/checkout/buy/f75bfb65-443a-46a0-abb1-981e0ff4b382', '_blank')}
>
<Zap className="w-4 h-4 mr-2" />
Get Personal Pro
Get Skipper
<ExternalLink className="w-3 h-3 ml-1.5 opacity-50" />
</Button>
</div>
)}
{/* Team Pro Card */}
{/* Admiral Card */}
<div className="border border-border rounded-xl p-4 space-y-3 bg-muted/5">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-500" />
<span className="font-semibold text-sm">Team Pro</span>
<span className="font-semibold text-sm">Admiral</span>
</div>
<p className="text-xs text-muted-foreground">For teams managing shared infrastructure.</p>
<ul className="space-y-1.5">
{[
...(license?.variant === 'personal' ? ['Everything in Personal Pro'] : ['Everything in Community']),
...(license?.variant === 'personal' ? ['Everything in Skipper'] : ['Everything in Community']),
'Unlimited admin accounts',
'Unlimited viewer accounts',
...(license?.variant !== 'personal' ? ['Fleet View & webhooks', 'Atomic deployment & backups'] : []),
@@ -1427,7 +1427,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
onClick={() => window.open('https://saelix.lemonsqueezy.com/checkout/buy/b049b824-176a-408d-a9d3-9365c979a61f', '_blank')}
>
<Zap className="w-4 h-4 mr-2" />
Get Team Pro
Get Admiral
<ExternalLink className="w-3 h-3 ml-1.5 opacity-50" />
</Button>
</div>
+2 -2
View File
@@ -11,8 +11,8 @@ interface TierBadgeProps {
const tierConfig = {
community: { icon: Globe, label: 'Community' },
pro: { icon: Crown, label: 'Pro' },
team: { icon: Users, label: 'Team' },
pro: { icon: Crown, label: 'Skipper' },
team: { icon: Users, label: 'Admiral' },
} as const;
function resolveTier(tier: LicenseTier, variant: LicenseVariant, status: LicenseStatus) {