mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +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:
@@ -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.' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user