From f841c402b2e75874b066400adadcd8dcdfa9ac5f Mon Sep 17 00:00:00 2001 From: Anso Date: Sun, 5 Apr 2026 07:00:21 -0400 Subject: [PATCH] fix(licensing): resolve Admiral variant detection and lifetime license handling (#376) * fix(licensing): resolve Admiral variant detection and lifetime license handling The Lemon Squeezy variant name for Admiral licenses contains "Admiral" (not "Team"), but getVariant() only checked for "team" and "personal". This caused Admiral licenses to be misidentified as Skipper, locking all Admiral-exclusive features. - Map "admiral" variant names to internal "team" value, "skipper" to "personal" - Add isLifetime field to LicenseInfo API response - Hide "Manage Subscription" button for lifetime licenses (no billing portal) - Show "Duration: Lifetime" instead of empty renewal date - Hide upgrade cards for active Admiral users - Add 23 unit tests covering variant resolution, tier computation, and lifetime detection - Add troubleshooting entries for wrong tier label, locked features, and billing portal errors * fix(licensing): address code review findings - Fix nested ternary in LicenseSection JSX; restore conditional rendering to avoid showing an empty "N/A" row for non-subscription states - Clean up test file: use shared svc variable, remove redundant comments, add trialDaysRemaining assertions, rename describe block --- CHANGELOG.md | 4 + backend/src/__tests__/license-service.test.ts | 234 ++++++++++++++++++ backend/src/services/LicenseService.ts | 13 +- docs/features/licensing.mdx | 8 +- .../licensing/license-admiral-active.png | Bin 0 -> 40221 bytes docs/operations/troubleshooting.mdx | 33 +++ .../components/settings/LicenseSection.tsx | 39 +-- frontend/src/context/LicenseContext.tsx | 1 + 8 files changed, 310 insertions(+), 22 deletions(-) create mode 100644 backend/src/__tests__/license-service.test.ts create mode 100644 docs/images/licensing/license-admiral-active.png diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c5ed93..f2f39c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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:** "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 ### Changed @@ -17,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **licensing:** `ProGate` component renamed to `PaidGate`, `isPro` renamed to `isPaid` throughout the codebase * **licensing:** backend error code `PRO_REQUIRED` renamed to `PAID_REQUIRED`; `requirePro` guard renamed to `requirePaid` * **docs:** all documentation updated to replace "Sencho Pro" references with proper tier names +* **licensing:** `LicenseInfo` API response now includes `isLifetime` boolean field ## [0.38.0](https://github.com/AnsoCode/Sencho/compare/v0.37.0...v0.38.0) (2026-04-04) diff --git a/backend/src/__tests__/license-service.test.ts b/backend/src/__tests__/license-service.test.ts new file mode 100644 index 00000000..60ce42ae --- /dev/null +++ b/backend/src/__tests__/license-service.test.ts @@ -0,0 +1,234 @@ +/** + * Tests for LicenseService: variant resolution, tier computation, lifetime detection, + * and getLicenseInfo() output across all license states. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb'; + +let tmpDir: string; +let svc: import('../services/LicenseService').LicenseService; +let DatabaseService: typeof import('../services/DatabaseService').DatabaseService; + +beforeAll(async () => { + tmpDir = await setupTestDb(); + const licMod = await import('../services/LicenseService'); + svc = licMod.LicenseService.getInstance(); + ({ DatabaseService } = await import('../services/DatabaseService')); +}); + +afterAll(() => { + cleanupTestDb(tmpDir); +}); + +function setLicenseState(overrides: Record) { + const db = DatabaseService.getInstance(); + const keys = [ + 'license_status', 'license_key', 'license_valid_until', + 'license_last_validated', 'license_customer_name', + 'license_product_name', 'license_variant_name', + 'billing_portal_url', 'billing_portal_expires', + ]; + for (const key of keys) { + db.setSystemState(key, ''); + } + for (const [key, value] of Object.entries(overrides)) { + db.setSystemState(key, value); + } +} + +describe('LicenseService.getVariant()', () => { + it('returns "personal" for trial licenses', () => { + setLicenseState({ license_status: 'trial' }); + expect(svc.getVariant()).toBe('personal'); + }); + + it('returns null when no variant name is stored', () => { + setLicenseState({ license_status: 'active' }); + expect(svc.getVariant()).toBeNull(); + }); + + it('maps "Team" variant name to "team"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Team' }); + expect(svc.getVariant()).toBe('team'); + }); + + it('maps "Personal" variant name to "personal"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Personal' }); + expect(svc.getVariant()).toBe('personal'); + }); + + it('maps "Admiral" variant name to "team"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Admiral' }); + expect(svc.getVariant()).toBe('team'); + }); + + it('maps "Admiral Lifetime" variant name to "team"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Admiral Lifetime' }); + expect(svc.getVariant()).toBe('team'); + }); + + it('maps "Skipper" variant name to "personal"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Skipper' }); + expect(svc.getVariant()).toBe('personal'); + }); + + it('maps "Skipper Lifetime" variant name to "personal"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Skipper Lifetime' }); + expect(svc.getVariant()).toBe('personal'); + }); + + it('defaults unknown variant names to "personal"', () => { + setLicenseState({ license_status: 'active', license_variant_name: 'Unknown Variant' }); + expect(svc.getVariant()).toBe('personal'); + }); +}); + +describe('LicenseService.getTier()', () => { + it('returns "community" when no status is set', () => { + setLicenseState({}); + // initialize() sets trial on first boot; override to test the empty-status path + DatabaseService.getInstance().setSystemState('license_status', ''); + expect(svc.getTier()).toBe('community'); + }); + + it('returns "community" for community status', () => { + setLicenseState({ license_status: 'community' }); + expect(svc.getTier()).toBe('community'); + }); + + it('returns "community" for expired status', () => { + setLicenseState({ license_status: 'expired' }); + expect(svc.getTier()).toBe('community'); + }); + + it('returns "community" for disabled status', () => { + setLicenseState({ license_status: 'disabled' }); + expect(svc.getTier()).toBe('community'); + }); + + it('returns "paid" for active status with valid license', () => { + setLicenseState({ + license_status: 'active', + license_last_validated: Date.now().toString(), + }); + expect(svc.getTier()).toBe('paid'); + }); + + it('returns "paid" for active trial', () => { + const future = new Date(); + future.setDate(future.getDate() + 7); + setLicenseState({ + license_status: 'trial', + license_valid_until: future.toISOString(), + }); + expect(svc.getTier()).toBe('paid'); + }); + + it('returns "community" for expired trial', () => { + const past = new Date(); + past.setDate(past.getDate() - 1); + setLicenseState({ + license_status: 'trial', + license_valid_until: past.toISOString(), + }); + expect(svc.getTier()).toBe('community'); + }); + + it('returns "paid" for lifetime license (no expiry)', () => { + setLicenseState({ + license_status: 'active', + license_key: 'test-key-1234', + license_last_validated: Date.now().toString(), + }); + expect(svc.getTier()).toBe('paid'); + }); +}); + +describe('LicenseService.getLicenseInfo() - isLifetime', () => { + it('sets isLifetime=true for active license with key and no expiry', () => { + setLicenseState({ + license_status: 'active', + license_key: 'test-key-1234', + license_last_validated: Date.now().toString(), + }); + const info = svc.getLicenseInfo(); + expect(info.isLifetime).toBe(true); + expect(info.trialDaysRemaining).toBeNull(); + }); + + it('sets isLifetime=false for active subscription with expiry', () => { + const future = new Date(); + future.setDate(future.getDate() + 30); + setLicenseState({ + license_status: 'active', + license_key: 'test-key-1234', + license_valid_until: future.toISOString(), + license_last_validated: Date.now().toString(), + }); + const info = svc.getLicenseInfo(); + expect(info.isLifetime).toBe(false); + expect(info.trialDaysRemaining).toBeNull(); + }); + + it('sets isLifetime=false for trial licenses', () => { + const future = new Date(); + future.setDate(future.getDate() + 14); + setLicenseState({ + license_status: 'trial', + license_valid_until: future.toISOString(), + }); + const info = svc.getLicenseInfo(); + expect(info.isLifetime).toBe(false); + expect(info.trialDaysRemaining).toBeGreaterThan(0); + }); + + it('sets isLifetime=false for community status', () => { + setLicenseState({ license_status: 'community' }); + const info = svc.getLicenseInfo(); + expect(info.isLifetime).toBe(false); + expect(info.trialDaysRemaining).toBeNull(); + }); +}); + +describe('LicenseService.getLicenseInfo() - full scenarios', () => { + it('returns correct info for an Admiral lifetime license', () => { + setLicenseState({ + license_status: 'active', + license_key: 'ABCD-EFGH-IJKL-MN5D', + license_variant_name: 'Admiral Lifetime', + license_customer_name: 'Test User', + license_product_name: 'Sencho Admiral', + license_last_validated: Date.now().toString(), + }); + const info = svc.getLicenseInfo(); + expect(info.tier).toBe('paid'); + expect(info.status).toBe('active'); + expect(info.variant).toBe('team'); + expect(info.isLifetime).toBe(true); + expect(info.trialDaysRemaining).toBeNull(); + expect(info.customerName).toBe('Test User'); + expect(info.productName).toBe('Sencho Admiral'); + expect(info.maskedKey).toBe('****-****-****-MN5D'); + }); + + it('returns correct info for a Skipper subscription', () => { + const future = new Date(); + future.setDate(future.getDate() + 30); + setLicenseState({ + license_status: 'active', + license_key: 'ABCD-EFGH-IJKL-SK5D', + license_variant_name: 'Skipper Monthly', + license_customer_name: 'Another User', + license_product_name: 'Sencho Skipper', + license_valid_until: future.toISOString(), + license_last_validated: Date.now().toString(), + }); + const info = svc.getLicenseInfo(); + expect(info.tier).toBe('paid'); + expect(info.status).toBe('active'); + expect(info.variant).toBe('personal'); + expect(info.isLifetime).toBe(false); + expect(info.trialDaysRemaining).toBeNull(); + expect(info.customerName).toBe('Another User'); + }); +}); diff --git a/backend/src/services/LicenseService.ts b/backend/src/services/LicenseService.ts index 3f701c6a..986676e8 100644 --- a/backend/src/services/LicenseService.ts +++ b/backend/src/services/LicenseService.ts @@ -33,6 +33,7 @@ export interface LicenseInfo { trialDaysRemaining: number | null; instanceId: string; portalUrl: string | null; + isLifetime: boolean; } /** Seat limits per variant. null = unlimited. */ @@ -194,7 +195,9 @@ export class LicenseService { /** * Get the license variant (personal or team) from stored metadata. - * Trial licenses default to "personal" - Admiral features require an Admiral license. + * 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. */ public getVariant(): LicenseVariant { const db = DatabaseService.getInstance(); @@ -203,8 +206,8 @@ export class LicenseService { const variantName = db.getSystemState('license_variant_name'); if (!variantName) return null; const lower = variantName.toLowerCase(); - if (lower.includes('team')) return 'team'; - if (lower.includes('personal')) return 'personal'; + 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 } @@ -233,6 +236,9 @@ export class LicenseService { trialDaysRemaining = Math.max(0, Math.ceil(remaining)); } + // Lifetime license: active with a stored key but no expiry date + const isLifetime = status === 'active' && !!key && !validUntil; + return { tier: this.getTier(), status, @@ -244,6 +250,7 @@ export class LicenseService { trialDaysRemaining, instanceId, portalUrl: db.getSystemState('billing_portal_url') || db.getSystemState('customer_portal_url') || null, + isLifetime, }; } diff --git a/docs/features/licensing.mdx b/docs/features/licensing.mdx index 90e8278d..25dd7188 100644 --- a/docs/features/licensing.mdx +++ b/docs/features/licensing.mdx @@ -75,9 +75,13 @@ Once your license is active, you can manage your subscription in two ways: - **From the profile menu:** Click your profile icon in the top bar and select **Billing** for quick access to the same portal. - License settings showing an active Admiral subscription with Manage Subscription button + License settings showing an active Admiral subscription with Manage Subscription button +### Lifetime licenses + +Lifetime licenses do not have a recurring subscription, so the **Manage Subscription** button is not shown. Instead, the license card displays **Duration: Lifetime** to confirm your license never expires. All other features (deactivation, multi-node enforcement, validation) work the same as subscription licenses. + ## Multi-node license enforcement If you manage multiple servers through Sencho's [multi-node feature](/features/multi-node), you only need a license on your **primary instance**. All remote nodes automatically inherit the primary's license tier for proxied requests. No per-node activation is required. @@ -93,3 +97,5 @@ To transfer your license to a different instance: 3. On your new instance, activate the same license key. Deactivation reverts the current instance to the Community tier immediately. + +Having trouble with your license? See the [Troubleshooting](/operations/troubleshooting#license-tier-shows-the-wrong-name) page for solutions to common licensing issues. diff --git a/docs/images/licensing/license-admiral-active.png b/docs/images/licensing/license-admiral-active.png new file mode 100644 index 0000000000000000000000000000000000000000..b6e9209bad4d31b6c53c919652b1cb242d32b376 GIT binary patch literal 40221 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 literal 0 HcmV?d00001 diff --git a/docs/operations/troubleshooting.mdx b/docs/operations/troubleshooting.mdx index 6dad324e..4c9169d8 100644 --- a/docs/operations/troubleshooting.mdx +++ b/docs/operations/troubleshooting.mdx @@ -154,6 +154,39 @@ To re-test connectivity after making changes, open **Profile > Settings > Nodes* --- +## License tier shows the wrong name + +**Symptom:** The license card in **Settings > License** shows the wrong tier name (e.g. "Sencho Skipper" when you purchased Admiral). + +**Cause:** Older versions of Sencho could misidentify certain license variant names from the payment provider. + +**Fix:** Update to the latest version of Sencho and restart. The correct tier name will appear automatically in **Settings > License**. + +--- + +## Paid features are still locked after activation + +**Symptom:** Features remain locked even though you activated a valid license key. + +**Checks in order:** + +1. Open **Settings > License** and verify it shows your license as **active** with the correct tier name. +2. If the tier shows "Skipper" but you purchased Admiral, this is the same variant identification issue described above. Update Sencho to the latest version to resolve it. +3. If the tier shows correctly but features are still locked, restart the Sencho container. + +--- + +## "No billing portal available" when clicking Manage Subscription + +**Symptom:** Clicking **Manage Subscription** shows an error about no billing portal being available. + +**Possible causes:** + +- **Lifetime license:** Lifetime licenses do not have a billing portal. This is expected behavior. In the latest version, the **Manage Subscription** button is hidden for lifetime licenses. +- **Network issues:** If your Sencho instance cannot reach the internet, the billing portal URL cannot be fetched. Check your instance's outbound connectivity and try again. + +--- + ## Paid features return 403 on remote nodes **Symptom:** A Skipper or Admiral feature works on the local node but returns a 403 error when you switch to a remote node. diff --git a/frontend/src/components/settings/LicenseSection.tsx b/frontend/src/components/settings/LicenseSection.tsx index 35f7a4aa..1d044907 100644 --- a/frontend/src/components/settings/LicenseSection.tsx +++ b/frontend/src/components/settings/LicenseSection.tsx @@ -42,8 +42,9 @@ export function LicenseSection() { } }; + const isAdmiral = isPaid && license?.variant === 'team' && license?.status === 'active'; const showSkipperCard = !isPaid || license?.status === 'trial'; - const showUpgradeCards = showSkipperCard || (license?.variant === 'personal' && license?.status === 'active'); + const showUpgradeCards = !isAdmiral && (showSkipperCard || (license?.variant === 'personal' && license?.status === 'active')); return (

@@ -95,10 +96,10 @@ export function LicenseSection() { {license.maskedKey}
)} - {license.validUntil && ( + {(license.isLifetime || license.validUntil) && (
- Renews - {new Date(license.validUntil).toLocaleDateString()} + {license.isLifetime ? 'Duration' : 'Renews'} + {license.isLifetime ? 'Lifetime' : new Date(license.validUntil!).toLocaleDateString()}
)} @@ -122,20 +123,22 @@ export function LicenseSection() { {/* Manage Subscription (active paid license) */} {license?.status === 'active' && (
- + {!license.isLifetime && ( + + )}

Deactivating will revert to Community features. diff --git a/frontend/src/context/LicenseContext.tsx b/frontend/src/context/LicenseContext.tsx index 33a22a0e..27d14bef 100644 --- a/frontend/src/context/LicenseContext.tsx +++ b/frontend/src/context/LicenseContext.tsx @@ -17,6 +17,7 @@ export interface LicenseInfo { trialDaysRemaining: number | null; instanceId: string; portalUrl: string | null; + isLifetime: boolean; } interface LicenseContextType {