diff --git a/CHANGELOG.md b/CHANGELOG.md index cc93c5fd..74d6b884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +* **licensing:** internal variant values renamed from `personal`/`team` to `skipper`/`admiral`, aligning code with user-facing tier names +* **licensing:** variant type is now resolved once at activation/validation and stored in DB, instead of string-matching the Lemon Squeezy variant name on every read; `variant_id` is also captured for future lookups +* **licensing:** pre-existing installs auto-migrate on first `getVariant()` call (resolves from stored name, persists the result) + ### Fixed * **licensing:** trial users can now see Skipper/Admiral purchase cards in Settings > License (previously hidden due to a condition bug) -* **licensing:** Admiral licenses (including lifetime) are now correctly identified as the "team" variant; previously, Lemon Squeezy variant names containing "Admiral" were not matched, causing the tier to display as "Skipper" and Admiral features to remain locked +* **licensing:** Admiral licenses (including lifetime) are now correctly identified; previously, Lemon Squeezy variant names containing "Admiral" were not matched, causing the tier to display as "Skipper" and Admiral features to remain locked * **licensing:** "Manage Subscription" button is now hidden for lifetime licenses, which have no billing portal by design * **licensing:** license card now shows "Duration: Lifetime" for lifetime licenses instead of an empty renewal date diff --git a/backend/src/__tests__/auth.test.ts b/backend/src/__tests__/auth.test.ts index c4991546..64971049 100644 --- a/backend/src/__tests__/auth.test.ts +++ b/backend/src/__tests__/auth.test.ts @@ -100,7 +100,7 @@ describe('POST /api/system/console-token', () => { beforeAll(async () => { const { LicenseService } = await import('../services/LicenseService'); vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid'); - vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('team'); + vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('admiral'); }); afterAll(() => { diff --git a/backend/src/__tests__/distributed-license.test.ts b/backend/src/__tests__/distributed-license.test.ts index a9f76b1f..b5367449 100644 --- a/backend/src/__tests__/distributed-license.test.ts +++ b/backend/src/__tests__/distributed-license.test.ts @@ -41,7 +41,7 @@ describe('authMiddleware - distributed license headers', () => { .get(PAID_ROUTE) .set('Authorization', `Bearer ${token}`) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'personal'); + .set('x-sencho-variant', 'skipper'); // Should NOT get 403 PAID_REQUIRED; the proxy tier assertion grants access expect(res.status).not.toBe(403); @@ -54,7 +54,7 @@ describe('authMiddleware - distributed license headers', () => { .get(PAID_ROUTE) .set('Authorization', `Bearer ${token}`) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'team'); + .set('x-sencho-variant', 'admiral'); // Local license is community in test env → should get 403 expect(res.status).toBe(403); @@ -125,24 +125,24 @@ describe('requirePaid - distributed license', () => { // ─── requireAdmiral guard ─────────────────────────────────────────────────── describe('requireAdmiral - distributed license', () => { - it('allows access when proxy asserts paid tier with team variant', async () => { + it('allows access when proxy asserts paid tier with admiral variant', async () => { const token = signToken({ scope: 'node_proxy' }); const res = await request(app) .get(ADMIRAL_ROUTE) .set('Authorization', `Bearer ${token}`) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'team'); + .set('x-sencho-variant', 'admiral'); expect(res.status).not.toBe(403); }); - it('blocks when proxy asserts paid tier with personal variant', async () => { + it('blocks when proxy asserts paid tier with skipper variant', async () => { const token = signToken({ scope: 'node_proxy' }); const res = await request(app) .get(ADMIRAL_ROUTE) .set('Authorization', `Bearer ${token}`) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'personal'); + .set('x-sencho-variant', 'skipper'); expect(res.status).toBe(403); expect(res.body.code).toBe('ADMIRAL_REQUIRED'); @@ -181,7 +181,7 @@ describe('Security - tier header injection', () => { .get(ADMIRAL_ROUTE) .set('Authorization', `Bearer ${token}`) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'team'); + .set('x-sencho-variant', 'admiral'); // User session → tier headers ignored → local community tier → 403 expect(res.status).toBe(403); @@ -191,7 +191,7 @@ describe('Security - tier header injection', () => { const res = await request(app) .get(PAID_ROUTE) .set('x-sencho-tier', 'paid') - .set('x-sencho-variant', 'team'); + .set('x-sencho-variant', 'admiral'); expect(res.status).toBe(401); }); diff --git a/backend/src/__tests__/license-service.test.ts b/backend/src/__tests__/license-service.test.ts index 60ce42ae..3e307c0f 100644 --- a/backend/src/__tests__/license-service.test.ts +++ b/backend/src/__tests__/license-service.test.ts @@ -26,6 +26,7 @@ function setLicenseState(overrides: Record) { 'license_status', 'license_key', 'license_valid_until', 'license_last_validated', 'license_customer_name', 'license_product_name', 'license_variant_name', + 'license_variant_type', 'license_variant_id', 'billing_portal_url', 'billing_portal_expires', ]; for (const key of keys) { @@ -37,9 +38,9 @@ function setLicenseState(overrides: Record) { } describe('LicenseService.getVariant()', () => { - it('returns "personal" for trial licenses', () => { + it('returns "skipper" for trial licenses', () => { setLicenseState({ license_status: 'trial' }); - expect(svc.getVariant()).toBe('personal'); + expect(svc.getVariant()).toBe('skipper'); }); it('returns null when no variant name is stored', () => { @@ -47,39 +48,51 @@ describe('LicenseService.getVariant()', () => { expect(svc.getVariant()).toBeNull(); }); - it('maps "Team" variant name to "team"', () => { + it('reads pre-resolved variant type from DB (admiral)', () => { + setLicenseState({ license_status: 'active', license_variant_type: 'admiral' }); + expect(svc.getVariant()).toBe('admiral'); + }); + + it('reads pre-resolved variant type from DB (skipper)', () => { + setLicenseState({ license_status: 'active', license_variant_type: 'skipper' }); + expect(svc.getVariant()).toBe('skipper'); + }); + + it('falls back to name resolution and persists type (Team -> admiral)', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Team' }); - expect(svc.getVariant()).toBe('team'); + expect(svc.getVariant()).toBe('admiral'); + expect(DatabaseService.getInstance().getSystemState('license_variant_type')).toBe('admiral'); }); - it('maps "Personal" variant name to "personal"', () => { + it('falls back to name resolution and persists type (Personal -> skipper)', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Personal' }); - expect(svc.getVariant()).toBe('personal'); + expect(svc.getVariant()).toBe('skipper'); + expect(DatabaseService.getInstance().getSystemState('license_variant_type')).toBe('skipper'); }); - it('maps "Admiral" variant name to "team"', () => { + it('maps "Admiral" variant name to "admiral"', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Admiral' }); - expect(svc.getVariant()).toBe('team'); + expect(svc.getVariant()).toBe('admiral'); }); - it('maps "Admiral Lifetime" variant name to "team"', () => { + it('maps "Admiral Lifetime" variant name to "admiral"', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Admiral Lifetime' }); - expect(svc.getVariant()).toBe('team'); + expect(svc.getVariant()).toBe('admiral'); }); - it('maps "Skipper" variant name to "personal"', () => { + it('maps "Skipper" variant name to "skipper"', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Skipper' }); - expect(svc.getVariant()).toBe('personal'); + expect(svc.getVariant()).toBe('skipper'); }); - it('maps "Skipper Lifetime" variant name to "personal"', () => { + it('maps "Skipper Lifetime" variant name to "skipper"', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Skipper Lifetime' }); - expect(svc.getVariant()).toBe('personal'); + expect(svc.getVariant()).toBe('skipper'); }); - it('defaults unknown variant names to "personal"', () => { + it('defaults unknown variant names to "skipper"', () => { setLicenseState({ license_status: 'active', license_variant_name: 'Unknown Variant' }); - expect(svc.getVariant()).toBe('personal'); + expect(svc.getVariant()).toBe('skipper'); }); }); @@ -203,7 +216,7 @@ describe('LicenseService.getLicenseInfo() - full scenarios', () => { const info = svc.getLicenseInfo(); expect(info.tier).toBe('paid'); expect(info.status).toBe('active'); - expect(info.variant).toBe('team'); + expect(info.variant).toBe('admiral'); expect(info.isLifetime).toBe(true); expect(info.trialDaysRemaining).toBeNull(); expect(info.customerName).toBe('Test User'); @@ -226,7 +239,7 @@ describe('LicenseService.getLicenseInfo() - full scenarios', () => { const info = svc.getLicenseInfo(); expect(info.tier).toBe('paid'); expect(info.status).toBe('active'); - expect(info.variant).toBe('personal'); + expect(info.variant).toBe('skipper'); expect(info.isLifetime).toBe(false); expect(info.trialDaysRemaining).toBeNull(); expect(info.customerName).toBe('Another User'); diff --git a/backend/src/__tests__/scheduler-service.test.ts b/backend/src/__tests__/scheduler-service.test.ts index e10b001d..90065287 100644 --- a/backend/src/__tests__/scheduler-service.test.ts +++ b/backend/src/__tests__/scheduler-service.test.ts @@ -29,7 +29,7 @@ const { mockInsertSnapshotFiles: vi.fn(), mockClearStackUpdateStatus: vi.fn(), mockGetTier: vi.fn().mockReturnValue('paid'), - mockGetVariant: vi.fn().mockReturnValue('team'), + mockGetVariant: vi.fn().mockReturnValue('admiral'), mockGetContainersByStack: vi.fn().mockResolvedValue([]), mockRestartContainer: vi.fn().mockResolvedValue(undefined), mockPruneSystem: vi.fn().mockResolvedValue({ success: true, reclaimedBytes: 0 }), @@ -198,7 +198,7 @@ describe('SchedulerService - license gating', () => { it('allows all actions for admiral (pro + team)', async () => { mockGetTier.mockReturnValue('paid'); - mockGetVariant.mockReturnValue('team'); + mockGetVariant.mockReturnValue('admiral'); mockGetDueScheduledTasks.mockReturnValue([makeTask({ action: 'restart' })]); mockGetContainersByStack.mockResolvedValue([{ Id: 'c1', Service: 'web' }]); @@ -215,7 +215,7 @@ describe('SchedulerService - license gating', () => { describe('SchedulerService - concurrent task prevention', () => { it('does not execute a task that is already in runningTasks', async () => { mockGetTier.mockReturnValue('paid'); - mockGetVariant.mockReturnValue('team'); + mockGetVariant.mockReturnValue('admiral'); mockGetDueScheduledTasks.mockReturnValue([{ id: 42, name: 'running-task', @@ -240,7 +240,7 @@ describe('SchedulerService - concurrent task prevention', () => { it('removes task from runningTasks after completion', async () => { mockGetTier.mockReturnValue('paid'); - mockGetVariant.mockReturnValue('team'); + mockGetVariant.mockReturnValue('admiral'); mockGetContainersByStack.mockResolvedValue([{ Id: 'c1', Service: 'web' }]); const svc = SchedulerService.getInstance(); @@ -612,7 +612,7 @@ describe('SchedulerService - error handling', () => { describe('SchedulerService - cleanup', () => { it('calls cleanupOldTaskRuns(30) on every tick', async () => { mockGetTier.mockReturnValue('paid'); - mockGetVariant.mockReturnValue('team'); + mockGetVariant.mockReturnValue('admiral'); mockGetDueScheduledTasks.mockReturnValue([]); const svc = SchedulerService.getInstance(); diff --git a/backend/src/index.ts b/backend/src/index.ts index f928872b..47e07675 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -861,7 +861,7 @@ const requireAdmiral = (req: Request, res: Response): boolean => { res.status(403).json({ error: 'This feature requires a Skipper or Admiral license.', code: 'PAID_REQUIRED' }); return false; } - if (variant !== 'team') { + if (variant !== 'admiral') { res.status(403).json({ error: 'This feature requires a Sencho Admiral license.', code: 'ADMIRAL_REQUIRED' }); return false; } @@ -936,7 +936,7 @@ function checkPermission( // Scoped assignments only apply when a resource is specified and license is Admiral if (!resourceType || !resourceId) return false; - if (LicenseService.getInstance().getVariant() !== 'team') return false; + if (LicenseService.getInstance().getVariant() !== 'admiral') return false; const assignments = DatabaseService.getInstance().getRoleAssignments(req.user.userId, resourceType, resourceId); for (const assignment of assignments) { @@ -2327,7 +2327,7 @@ app.get('/api/permissions/me', authMiddleware, (req: Request, res: Response): vo globalRole, globalPermissions, scopedPermissions, - isAdmiral: LicenseService.getInstance().getVariant() === 'team', + isAdmiral: LicenseService.getInstance().getVariant() === 'admiral', }); } catch (error) { console.error('[Permissions] Error:', error); @@ -2628,7 +2628,7 @@ server.on('upgrade', async (req, socket, head) => { } // Admiral license gate: host console requires Admiral (paid + team variant) const ls = LicenseService.getInstance(); - if (ls.getTier() !== 'paid' || ls.getVariant() !== 'team') { + if (ls.getTier() !== 'paid' || ls.getVariant() !== 'admiral') { socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); socket.destroy(); return; @@ -4382,7 +4382,7 @@ app.get('/api/scheduled-tasks', (req: Request, res: Response): void => { let tasks = DatabaseService.getInstance().getScheduledTasks(); // Skipper users only see 'update' tasks; Admiral sees all const ls = LicenseService.getInstance(); - if (ls.getVariant() !== 'team') { + if (ls.getVariant() !== 'admiral') { tasks = tasks.filter(t => t.action === 'update'); } res.json(tasks); diff --git a/backend/src/services/LicenseService.ts b/backend/src/services/LicenseService.ts index 986676e8..27ec1c27 100644 --- a/backend/src/services/LicenseService.ts +++ b/backend/src/services/LicenseService.ts @@ -5,10 +5,10 @@ import { DatabaseService } from './DatabaseService'; export type LicenseTier = 'community' | 'paid'; export type LicenseStatus = 'community' | 'trial' | 'active' | 'expired' | 'disabled'; -export type LicenseVariant = 'personal' | 'team' | null; +export type LicenseVariant = 'skipper' | 'admiral' | null; const VALID_TIERS: readonly string[] = ['community', 'paid'] satisfies readonly LicenseTier[]; -const VALID_VARIANTS: readonly string[] = ['personal', 'team'] satisfies readonly LicenseVariant[]; +const VALID_VARIANTS: readonly string[] = ['skipper', 'admiral'] satisfies readonly LicenseVariant[]; export function isLicenseTier(value: unknown): value is LicenseTier { return typeof value === 'string' && (VALID_TIERS as readonly string[]).includes(value); @@ -43,8 +43,8 @@ export interface SeatLimits { } const SEAT_LIMITS: Record = { - personal: { maxAdmins: 1, maxViewers: 3 }, - team: { maxAdmins: null, maxViewers: null }, + skipper: { maxAdmins: 1, maxViewers: 3 }, + admiral: { maxAdmins: null, maxViewers: null }, }; interface LemonSqueezyActivationResponse { @@ -194,21 +194,49 @@ export class LicenseService { } /** - * Get the license variant (personal or team) from stored metadata. - * Trial licenses default to "personal"; Admiral features require an Admiral license. - * Maps Lemon Squeezy variant names (which may use brand names like "Admiral" - * or "Skipper") to the internal 'team' or 'personal' values. + * Resolve a Lemon Squeezy variant name string to the internal variant type. + * Handles legacy names ("Team", "Personal") and current names ("Admiral", "Skipper"). + */ + private resolveVariantType(variantName: string): 'skipper' | 'admiral' { + const lower = variantName.toLowerCase(); + if (lower.includes('team') || lower.includes('admiral')) return 'admiral'; + if (lower.includes('personal') || lower.includes('skipper')) return 'skipper'; + return 'skipper'; + } + + /** Persist variant metadata from Lemon Squeezy response to DB. */ + private storeVariantMeta(db: DatabaseService, meta: { variant_name?: string; variant_id?: number }): void { + if (meta.variant_name) { + db.setSystemState('license_variant_name', meta.variant_name); + db.setSystemState('license_variant_type', this.resolveVariantType(meta.variant_name)); + } + if (meta.variant_id) { + db.setSystemState('license_variant_id', String(meta.variant_id)); + } + } + + /** + * Get the license variant (skipper or admiral) from stored metadata. + * Trial licenses default to "skipper"; Admiral features require an Admiral license. + * Reads the pre-resolved `license_variant_type` from DB. Falls back to name-based + * resolution for pre-existing installs, then persists the result so the fallback + * only runs once. */ public getVariant(): LicenseVariant { const db = DatabaseService.getInstance(); const status = db.getSystemState('license_status'); - if (status === 'trial') return 'personal'; + if (status === 'trial') return 'skipper'; + + // Primary path: read the pre-resolved type stored at activation/validation + const storedType = db.getSystemState('license_variant_type'); + if (isLicenseVariant(storedType)) return storedType; + + // Backward compat: resolve from variant name for pre-existing installs const variantName = db.getSystemState('license_variant_name'); if (!variantName) return null; - const lower = variantName.toLowerCase(); - if (lower.includes('team') || lower.includes('admiral')) return 'team'; - if (lower.includes('personal') || lower.includes('skipper')) return 'personal'; - return 'personal'; // default activated licenses to personal + const resolved = this.resolveVariantType(variantName); + db.setSystemState('license_variant_type', resolved); + return resolved; } /** @@ -217,7 +245,7 @@ export class LicenseService { public getSeatLimits(): SeatLimits { const variant = this.getVariant(); if (!variant) return { maxAdmins: 1, maxViewers: 0 }; // community - return SEAT_LIMITS[variant] || SEAT_LIMITS.personal; + return SEAT_LIMITS[variant] || SEAT_LIMITS.skipper; } /** @@ -295,8 +323,8 @@ export class LicenseService { if (data.meta?.product_name) { db.setSystemState('license_product_name', data.meta.product_name); } - if (data.meta?.variant_name) { - db.setSystemState('license_variant_name', data.meta.variant_name); + if (data.meta) { + this.storeVariantMeta(db, data.meta); } if (data.meta?.customer_id) { db.setSystemState('customer_id', String(data.meta.customer_id)); @@ -353,6 +381,8 @@ export class LicenseService { 'license_customer_name', 'license_product_name', 'license_variant_name', + 'license_variant_type', + 'license_variant_id', 'subscription_id', 'customer_id', 'customer_portal_url', @@ -428,8 +458,8 @@ export class LicenseService { if (data.meta?.product_name) { db.setSystemState('license_product_name', data.meta.product_name); } - if (data.meta?.variant_name) { - db.setSystemState('license_variant_name', data.meta.variant_name); + if (data.meta) { + this.storeVariantMeta(db, data.meta); } if (data.meta?.customer_id && !db.getSystemState('customer_id')) { db.setSystemState('customer_id', String(data.meta.customer_id)); diff --git a/backend/src/services/SchedulerService.ts b/backend/src/services/SchedulerService.ts index 0238f362..7dae2717 100644 --- a/backend/src/services/SchedulerService.ts +++ b/backend/src/services/SchedulerService.ts @@ -50,7 +50,7 @@ export class SchedulerService { try { const ls = LicenseService.getInstance(); const isPaid = ls.getTier() === 'paid'; - const isAdmiral = isPaid && ls.getVariant() === 'team'; + const isAdmiral = isPaid && ls.getVariant() === 'admiral'; if (!isPaid) return; // No scheduled tasks for unpaid tiers const db = DatabaseService.getInstance(); diff --git a/frontend/src/components/AdmiralGate.tsx b/frontend/src/components/AdmiralGate.tsx index acae7724..c8580628 100644 --- a/frontend/src/components/AdmiralGate.tsx +++ b/frontend/src/components/AdmiralGate.tsx @@ -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 ( diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 8acef651..2536d9c1 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -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 }); diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 66731d1a..9784dc8a 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -303,7 +303,7 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal } }; - const isAdmiral = isPaid && license?.variant === 'team'; + const isAdmiral = isPaid && license?.variant === 'admiral'; return ( !open && onClose()}> diff --git a/frontend/src/components/TierBadge.tsx b/frontend/src/components/TierBadge.tsx index ffb53da9..a6cd654d 100644 --- a/frontend/src/components/TierBadge.tsx +++ b/frontend/src/components/TierBadge.tsx @@ -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; } diff --git a/frontend/src/components/settings/DeveloperSection.tsx b/frontend/src/components/settings/DeveloperSection.tsx index 24f071d9..9c1ea2c0 100644 --- a/frontend/src/components/settings/DeveloperSection.tsx +++ b/frontend/src/components/settings/DeveloperSection.tsx @@ -143,7 +143,7 @@ export function DeveloperSection({ settings, onSettingChange, onSave, isSaving, - {isPaid && license?.variant === 'team' && ( + {isPaid && license?.variant === 'admiral' && (
diff --git a/frontend/src/components/settings/LicenseSection.tsx b/frontend/src/components/settings/LicenseSection.tsx index 1d044907..b8839d8b 100644 --- a/frontend/src/components/settings/LicenseSection.tsx +++ b/frontend/src/components/settings/LicenseSection.tsx @@ -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 (
@@ -210,9 +210,9 @@ export function LicenseSection() {

For teams managing shared infrastructure.