diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index d8e2fb710..e6c697614 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -2299,6 +2299,7 @@ export class PostHogAPIClient { try { const url = new URL(`${this.api.baseUrl}/api/seats/me/`); url.searchParams.set("product_key", SEAT_PRODUCT_KEY); + url.searchParams.set("best", "true"); const response = await this.api.fetcher.fetch({ method: "get", url, diff --git a/apps/code/src/renderer/features/billing/stores/seatStore.test.ts b/apps/code/src/renderer/features/billing/stores/seatStore.test.ts index e375a71eb..b2e4cffb3 100644 --- a/apps/code/src/renderer/features/billing/stores/seatStore.test.ts +++ b/apps/code/src/renderer/features/billing/stores/seatStore.test.ts @@ -37,10 +37,6 @@ vi.mock("@utils/logger", () => ({ }, })); -vi.mock("@utils/urls", () => ({ - getPostHogUrl: (path: string) => `https://posthog.com${path}`, -})); - vi.mock("@renderer/trpc", () => ({ trpcClient: { llmGateway: { @@ -92,6 +88,7 @@ describe("seatStore", () => { isLoading: false, error: null, redirectUrl: null, + billingOrgId: null, }); }); @@ -260,9 +257,7 @@ describe("seatStore", () => { const state = useSeatStore.getState(); expect(state.error).toBe("Billing subscription required"); - expect(state.redirectUrl).toBe( - "https://posthog.com/organization/billing", - ); + expect(state.redirectUrl).toBe("/organization/billing"); }); it("sets error on payment failure", async () => { diff --git a/apps/code/src/renderer/features/billing/stores/seatStore.ts b/apps/code/src/renderer/features/billing/stores/seatStore.ts index 46a1b92bf..531ab5c01 100644 --- a/apps/code/src/renderer/features/billing/stores/seatStore.ts +++ b/apps/code/src/renderer/features/billing/stores/seatStore.ts @@ -8,7 +8,6 @@ import type { SeatData } from "@shared/types/seat"; import { PLAN_FREE, PLAN_PRO } from "@shared/types/seat"; import { logger } from "@utils/logger"; import { queryClient } from "@utils/queryClient"; -import { getPostHogUrl } from "@utils/urls"; import { create } from "zustand"; const log = logger.scope("seat-store"); @@ -18,6 +17,7 @@ interface SeatStoreState { isLoading: boolean; error: string | null; redirectUrl: string | null; + billingOrgId: string | null; } interface SeatStoreActions { @@ -54,7 +54,7 @@ function handleSeatError( set({ isLoading: false, error: "Billing subscription required", - redirectUrl: getPostHogUrl("/organization/billing"), + redirectUrl: error.redirectUrl, }); return; } @@ -80,6 +80,7 @@ const initialState: SeatStoreState = { isLoading: false, error: null, redirectUrl: null, + billingOrgId: null, }; export const useSeatStore = create()((set, get) => ({ @@ -99,7 +100,11 @@ export const useSeatStore = create()((set, get) => ({ seat = await client.getMySeat(); } } - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat?.organization_id ?? null, + }); } catch (error) { const { seat: existingSeat } = get(); if (existingSeat) { @@ -122,12 +127,20 @@ export const useSeatStore = create()((set, get) => ({ plan: existing.plan_key, status: existing.status, }); - set({ seat: existing, isLoading: false }); + set({ + seat: existing, + isLoading: false, + billingOrgId: existing.organization_id ?? null, + }); return; } const seat = await client.createSeat(PLAN_FREE); log.info("Free seat created", { id: seat.id, plan: seat.plan_key }); - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat.organization_id ?? null, + }); invalidatePlanCache(); } catch (error) { log.error("provisionFreeSeat failed", error); @@ -142,16 +155,28 @@ export const useSeatStore = create()((set, get) => ({ const existing = await client.getMySeat(); if (existing) { if (existing.plan_key === PLAN_PRO) { - set({ seat: existing, isLoading: false }); + set({ + seat: existing, + isLoading: false, + billingOrgId: existing.organization_id ?? null, + }); return; } const seat = await client.upgradeSeat(PLAN_PRO); - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat.organization_id ?? null, + }); invalidatePlanCache(); return; } const seat = await client.createSeat(PLAN_PRO); - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat.organization_id ?? null, + }); invalidatePlanCache(); } catch (error) { handleSeatError(error, set); @@ -164,7 +189,11 @@ export const useSeatStore = create()((set, get) => ({ const client = await getClient(); await client.cancelSeat(); const seat = await client.getMySeat(); - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat?.organization_id ?? null, + }); invalidatePlanCache(); } catch (error) { handleSeatError(error, set); @@ -176,7 +205,11 @@ export const useSeatStore = create()((set, get) => ({ try { const client = await getClient(); const seat = await client.reactivateSeat(); - set({ seat, isLoading: false }); + set({ + seat, + isLoading: false, + billingOrgId: seat.organization_id ?? null, + }); invalidatePlanCache(); } catch (error) { handleSeatError(error, set); diff --git a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx index 17b301cd7..f86802341 100644 --- a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx @@ -1,3 +1,4 @@ +import { getAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { useUsage } from "@features/billing/hooks/useUsage"; import { useSeatStore } from "@features/billing/stores/seatStore"; @@ -20,9 +21,27 @@ import { } from "@radix-ui/themes"; import { Tooltip } from "@renderer/components/ui/Tooltip"; import { PLAN_PRO_ALPHA } from "@shared/types/seat"; +import { logger } from "@utils/logger"; import { getPostHogUrl } from "@utils/urls"; import { useEffect, useState } from "react"; +const log = logger.scope("plan-usage"); + +async function openBillingPage(orgId: string | null): Promise { + if (orgId) { + try { + const client = await getAuthenticatedClient(); + if (client) { + await client.switchOrganization(orgId); + } + } catch (err) { + log.warn("Failed to switch org before opening billing", err); + } + } + const url = getPostHogUrl("/organization/billing"); + if (url) window.open(url, "_blank"); +} + function formatResetTime(seconds: number): string { if (seconds < 3600) return "less than 1 hour"; if (seconds < 86400) { @@ -43,11 +62,15 @@ export function PlanUsageSettings() { isLoading, error, redirectUrl, + billingOrgId, } = useSeat(); const { fetchSeat, upgradeToPro, cancelSeat, reactivateSeat, clearError } = useSeatStore(); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const billingUrl = getPostHogUrl("/organization/billing", cloudRegion); + const redirectFullUrl = redirectUrl + ? getPostHogUrl(redirectUrl, cloudRegion) + : null; const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const isAlpha = seat?.plan_key === PLAN_PRO_ALPHA; @@ -104,8 +127,9 @@ export function PlanUsageSettings() { size="1" variant="outline" color="amber" + disabled={!redirectFullUrl} onClick={() => { - window.open(redirectUrl, "_blank"); + if (redirectFullUrl) window.open(redirectFullUrl, "_blank"); clearError(); }} className="self-start" @@ -279,7 +303,7 @@ export function PlanUsageSettings() { variant="outline" disabled={!billingUrl} onClick={() => { - if (billingUrl) window.open(billingUrl, "_blank"); + void openBillingPage(billingOrgId); }} > Open diff --git a/apps/code/src/renderer/hooks/useSeat.ts b/apps/code/src/renderer/hooks/useSeat.ts index 0e65899d9..a3ff0883f 100644 --- a/apps/code/src/renderer/hooks/useSeat.ts +++ b/apps/code/src/renderer/hooks/useSeat.ts @@ -6,6 +6,7 @@ export function useSeat() { const isLoading = useSeatStore((s) => s.isLoading); const error = useSeatStore((s) => s.error); const redirectUrl = useSeatStore((s) => s.redirectUrl); + const billingOrgId = useSeatStore((s) => s.billingOrgId); const isPro = isProPlan(seat?.plan_key); const hasAccess = seat ? seatHasAccess(seat.status) : false; @@ -20,6 +21,7 @@ export function useSeat() { isLoading, error, redirectUrl, + billingOrgId, isPro, hasAccess, isCanceling, diff --git a/apps/code/src/shared/types/seat.ts b/apps/code/src/shared/types/seat.ts index 7cd5d6f6f..5ff3d88a5 100644 --- a/apps/code/src/shared/types/seat.ts +++ b/apps/code/src/shared/types/seat.ts @@ -16,6 +16,8 @@ export interface SeatData { created_at: number; active_until: number | null; active_from: number; + organization_id?: string; + organization_name?: string; } export const SEAT_PRODUCT_KEY = "posthog_code";