feat(billing): add direct Stripe billing portal access

Replace dashboard redirect with Stripe Customer Portal session for the
"Manage in Stripe" button, so users go directly to Stripe to manage
invoices, payment methods, and billing details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
tusharmagar 2026-04-09 11:22:24 +05:30
parent 9c010dabd8
commit 309c05782e
4 changed files with 34 additions and 4 deletions

View file

@ -40,7 +40,7 @@ import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedu
import { search } from '@x/core/dist/search/search.js'; import { search } from '@x/core/dist/search/search.js';
import { versionHistory, voice } from '@x/core'; import { versionHistory, voice } from '@x/core';
import { classifySchedule, processRowboatInstruction } from '@x/core/dist/knowledge/inline_tasks.js'; import { classifySchedule, processRowboatInstruction } from '@x/core/dist/knowledge/inline_tasks.js';
import { getBillingInfo } from '@x/core/dist/billing/billing.js'; import { getBillingInfo, getBillingPortalUrl } from '@x/core/dist/billing/billing.js';
import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js'; import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js';
import { getAccessToken } from '@x/core/dist/auth/tokens.js'; import { getAccessToken } from '@x/core/dist/auth/tokens.js';
import { getRowboatConfig } from '@x/core/dist/config/rowboat.js'; import { getRowboatConfig } from '@x/core/dist/config/rowboat.js';
@ -759,5 +759,9 @@ export function setupIpcHandlers() {
'billing:getInfo': async () => { 'billing:getInfo': async () => {
return await getBillingInfo(); return await getBillingInfo();
}, },
'billing:getPortalUrl': async () => {
const url = await getBillingPortalUrl();
return { url };
},
}); });
} }

View file

@ -179,8 +179,8 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
<p className="text-xs text-muted-foreground">Subscribe to access AI features</p> <p className="text-xs text-muted-foreground">Subscribe to access AI features</p>
)} )}
</div> </div>
<Button variant="outline" size="sm" onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)}> <Button variant="outline" size="sm" onClick={() => appUrl && window.open(appUrl)}>
{!billing.subscriptionPlan ? 'Subscribe' : 'Change plan'} View plan
</Button> </Button>
</div> </div>
</div> </div>
@ -204,7 +204,14 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
variant="outline" variant="outline"
size="sm" size="sm"
disabled={!billing?.subscriptionPlan} disabled={!billing?.subscriptionPlan}
onClick={() => appUrl && window.open(appUrl)} onClick={async () => {
try {
const { url } = await window.ipc.invoke('billing:getPortalUrl', null);
window.open(url);
} catch {
toast.error('Failed to open billing portal');
}
}}
className="gap-1.5" className="gap-1.5"
> >
<ExternalLink className="size-3" /> <ExternalLink className="size-3" />

View file

@ -11,6 +11,19 @@ export interface BillingInfo {
availableCredits: number; availableCredits: number;
} }
export async function getBillingPortalUrl(): Promise<string> {
const accessToken = await getAccessToken();
const response = await fetch(`${API_URL}/v1/billing/portal-session`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
throw new Error(`Portal session failed: ${response.status}`);
}
const body = await response.json() as { url: string };
return body.url;
}
export async function getBillingInfo(): Promise<BillingInfo> { export async function getBillingInfo(): Promise<BillingInfo> {
const accessToken = await getAccessToken(); const accessToken = await getAccessToken();
const response = await fetch(`${API_URL}/v1/me`, { const response = await fetch(`${API_URL}/v1/me`, {

View file

@ -572,6 +572,12 @@ const ipcSchemas = {
availableCredits: z.number(), availableCredits: z.number(),
}), }),
}, },
'billing:getPortalUrl': {
req: z.null(),
res: z.object({
url: z.string(),
}),
},
} as const; } as const;
// ============================================================================ // ============================================================================