From 309c05782e3c16ce14b0fdded5b3c24e36956bb8 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 9 Apr 2026 11:22:24 +0530 Subject: [PATCH] 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) --- apps/x/apps/main/src/ipc.ts | 6 +++++- .../src/components/settings/account-settings.tsx | 13 ++++++++++--- apps/x/packages/core/src/billing/billing.ts | 13 +++++++++++++ apps/x/packages/shared/src/ipc.ts | 6 ++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index e05b57b3..ad173678 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -40,7 +40,7 @@ import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedu import { search } from '@x/core/dist/search/search.js'; import { versionHistory, voice } from '@x/core'; 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 { getAccessToken } from '@x/core/dist/auth/tokens.js'; import { getRowboatConfig } from '@x/core/dist/config/rowboat.js'; @@ -759,5 +759,9 @@ export function setupIpcHandlers() { 'billing:getInfo': async () => { return await getBillingInfo(); }, + 'billing:getPortalUrl': async () => { + const url = await getBillingPortalUrl(); + return { url }; + }, }); } diff --git a/apps/x/apps/renderer/src/components/settings/account-settings.tsx b/apps/x/apps/renderer/src/components/settings/account-settings.tsx index 1860305d..32d732ba 100644 --- a/apps/x/apps/renderer/src/components/settings/account-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/account-settings.tsx @@ -179,8 +179,8 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {

Subscribe to access AI features

)} - @@ -204,7 +204,14 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { variant="outline" size="sm" 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" > diff --git a/apps/x/packages/core/src/billing/billing.ts b/apps/x/packages/core/src/billing/billing.ts index 2365364d..3e05feec 100644 --- a/apps/x/packages/core/src/billing/billing.ts +++ b/apps/x/packages/core/src/billing/billing.ts @@ -11,6 +11,19 @@ export interface BillingInfo { availableCredits: number; } +export async function getBillingPortalUrl(): Promise { + 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 { const accessToken = await getAccessToken(); const response = await fetch(`${API_URL}/v1/me`, { diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index a8709aa2..5601f349 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -572,6 +572,12 @@ const ipcSchemas = { availableCredits: z.number(), }), }, + 'billing:getPortalUrl': { + req: z.null(), + res: z.object({ + url: z.string(), + }), + }, } as const; // ============================================================================