mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix(billing): hide billing portal for lifetime licenses (#427)
Lifetime licenses have no recurring subscription, so the Lemon Squeezy
customer portal cannot generate a URL. The Manage Subscription button in
Settings already had the isLifetime guard, but the Billing button in the
profile dropdown did not, causing a confusing "No billing portal
available" error.
- Add !license.isLifetime guard to UserProfileDropdown (matches
LicenseSection pattern)
- Move lifetime detection into getBillingPortalUrl() so the service owns
all billing eligibility logic
- Change return type to { url } | { error } discriminated union for
clear error propagation
This commit is contained in:
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
* **console:** fix remote node Host Console failing with "Connection error" / 502. The gateway's console-token request to the remote node was missing license tier headers, causing the remote's Admiral license gate to reject the request. Both the HTTP fetch and the WS upgrade handler now correctly propagate proxy tier headers for console_session tokens.
|
||||
* **billing:** hide the Billing button in the profile dropdown for lifetime licenses. Previously it was shown but always failed with "No billing portal available" because lifetime licenses have no recurring subscription. The Manage Subscription button in Settings already had this guard; now both entry points are consistent.
|
||||
* **auto-update:** resolve failure when executing auto-update policies on remote Distributed API nodes. Previously, the scheduler tried to access the remote Docker daemon directly, which is not supported. Now the update execution is proxied to the remote Sencho instance via HTTP, matching the existing Distributed API architecture.
|
||||
* **schedules:** auto-update policies no longer appear in the Scheduled Operations list. Each view now fetches only its relevant task type via server-side action filtering.
|
||||
* **console:** fix remote node Console returning 502 by injecting proxy tier headers into console-token fetch and WS upgrade handler.
|
||||
|
||||
@@ -1073,12 +1073,12 @@ app.post('/api/license/validate', async (_req: Request, res: Response): Promise<
|
||||
|
||||
app.get('/api/license/billing-portal', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const url = await LicenseService.getInstance().getBillingPortalUrl();
|
||||
if (!url) {
|
||||
res.status(404).json({ error: 'No billing portal available. Ensure you have an active license.' });
|
||||
const result = await LicenseService.getInstance().getBillingPortalUrl();
|
||||
if ('error' in result) {
|
||||
res.status(404).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.json({ url });
|
||||
res.json({ url: result.url });
|
||||
} catch (error) {
|
||||
console.error('[License] Billing portal error:', error);
|
||||
res.status(500).json({ error: 'Failed to retrieve billing portal URL' });
|
||||
|
||||
@@ -506,20 +506,26 @@ export class LicenseService {
|
||||
* Returns a pre-signed Lemon Squeezy Customer Portal URL (valid 24hrs).
|
||||
* Caches the URL for 12 hours to reduce external API calls.
|
||||
*/
|
||||
public async getBillingPortalUrl(): Promise<string | null> {
|
||||
public async getBillingPortalUrl(): Promise<{ url: string } | { error: string }> {
|
||||
const db = DatabaseService.getInstance();
|
||||
const status = db.getSystemState('license_status');
|
||||
const licenseKey = db.getSystemState('license_key');
|
||||
|
||||
if (status !== 'active' || !licenseKey) {
|
||||
return null;
|
||||
return { error: 'No billing portal available. Ensure you have an active license.' };
|
||||
}
|
||||
|
||||
// Lifetime licenses have no recurring subscription to manage
|
||||
const validUntil = db.getSystemState('license_valid_until');
|
||||
if (!validUntil) {
|
||||
return { error: 'Billing portal is not available for lifetime licenses.' };
|
||||
}
|
||||
|
||||
// Check cache (12hr TTL)
|
||||
const cachedUrl = db.getSystemState('billing_portal_url');
|
||||
const cachedExpires = db.getSystemState('billing_portal_expires');
|
||||
if (cachedUrl && cachedExpires && Date.now() < parseInt(cachedExpires, 10)) {
|
||||
return cachedUrl;
|
||||
return { url: cachedUrl };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -531,7 +537,7 @@ export class LicenseService {
|
||||
|
||||
const url = response.data?.url;
|
||||
if (!url) {
|
||||
return null;
|
||||
return { error: 'No billing portal available. Ensure you have an active license.' };
|
||||
}
|
||||
|
||||
// Cache for 12 hours
|
||||
@@ -539,12 +545,12 @@ export class LicenseService {
|
||||
db.setSystemState('billing_portal_url', url);
|
||||
db.setSystemState('billing_portal_expires', String(Date.now() + ttl));
|
||||
|
||||
return url;
|
||||
return { url };
|
||||
} catch (err) {
|
||||
console.warn('[License] Failed to fetch billing portal URL:', (err as Error).message);
|
||||
// Return stale cache if available
|
||||
if (cachedUrl) return cachedUrl;
|
||||
return null;
|
||||
if (cachedUrl) return { url: cachedUrl };
|
||||
return { error: 'Failed to retrieve billing portal URL.' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Active licenses are re-validated every **72 hours** against the Lemon Squeezy AP
|
||||
Once your license is active, you can manage your subscription in two ways:
|
||||
|
||||
- **From the License page:** Go to **Settings > License** and click **Manage Subscription**. This opens the Lemon Squeezy billing portal where you can update your payment method, view invoices, cancel, or switch plans.
|
||||
- **From the profile menu:** Click your profile icon in the top bar and select **Billing** for quick access to the same portal.
|
||||
- **From the profile menu:** Click your profile icon in the top bar and select **Billing** for quick access to the same portal. This option is hidden for lifetime licenses.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/licensing/license-admiral-active.png" alt="License settings showing an active Admiral subscription with Manage Subscription button" />
|
||||
|
||||
@@ -77,7 +77,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro
|
||||
<Settings className="w-4 h-4 text-muted-foreground" />
|
||||
Settings
|
||||
</button>
|
||||
{license?.status === 'active' && (
|
||||
{license?.status === 'active' && !license?.isLifetime && (
|
||||
<button
|
||||
onClick={openBillingPortal}
|
||||
disabled={billingLoading}
|
||||
|
||||
Reference in New Issue
Block a user