mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
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:
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user