Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2260,6 +2260,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -92,6 +88,7 @@ describe("seatStore", () => {
isLoading: false,
error: null,
redirectUrl: null,
billingOrgId: null,
});
});

Expand Down Expand Up @@ -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 () => {
Expand Down
53 changes: 43 additions & 10 deletions apps/code/src/renderer/features/billing/stores/seatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -18,6 +17,7 @@ interface SeatStoreState {
isLoading: boolean;
error: string | null;
redirectUrl: string | null;
billingOrgId: string | null;
}

interface SeatStoreActions {
Expand Down Expand Up @@ -54,7 +54,7 @@ function handleSeatError(
set({
isLoading: false,
error: "Billing subscription required",
redirectUrl: getPostHogUrl("/organization/billing"),
redirectUrl: error.redirectUrl,
});
return;
}
Expand All @@ -80,6 +80,7 @@ const initialState: SeatStoreState = {
isLoading: false,
error: null,
redirectUrl: null,
billingOrgId: null,
};

export const useSeatStore = create<SeatStore>()((set, get) => ({
Expand All @@ -99,7 +100,11 @@ export const useSeatStore = create<SeatStore>()((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) {
Expand All @@ -122,12 +127,20 @@ export const useSeatStore = create<SeatStore>()((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);
Expand All @@ -142,16 +155,28 @@ export const useSeatStore = create<SeatStore>()((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);
Expand All @@ -164,7 +189,11 @@ export const useSeatStore = create<SeatStore>()((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);
Expand All @@ -176,7 +205,11 @@ export const useSeatStore = create<SeatStore>()((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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useUsage } from "@features/billing/hooks/useUsage";
import { useSeatStore } from "@features/billing/stores/seatStore";
import { useSeat } from "@hooks/useSeat";
Expand All @@ -19,9 +20,26 @@ 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<void> {
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);
}
}
window.open(getPostHogUrl("/organization/billing"), "_blank");
}

function formatResetTime(seconds: number): string {
if (seconds < 3600) return "less than 1 hour";
if (seconds < 86400) {
Expand All @@ -42,6 +60,7 @@ export function PlanUsageSettings() {
isLoading,
error,
redirectUrl,
billingOrgId,
} = useSeat();
const { fetchSeat, upgradeToPro, cancelSeat, reactivateSeat, clearError } =
useSeatStore();
Expand Down Expand Up @@ -102,7 +121,7 @@ export function PlanUsageSettings() {
variant="outline"
color="amber"
onClick={() => {
window.open(redirectUrl, "_blank");
window.open(getPostHogUrl(redirectUrl), "_blank");
clearError();
}}
className="self-start"
Expand Down Expand Up @@ -275,8 +294,7 @@ export function PlanUsageSettings() {
size="1"
variant="outline"
onClick={() => {
const url = getPostHogUrl("/organization/billing");
window.open(url, "_blank");
void openBillingPage(billingOrgId);
}}
>
Open
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/renderer/hooks/useSeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,7 @@ export function useSeat() {
isLoading,
error,
redirectUrl,
billingOrgId,
isPro,
hasAccess,
isCanceling,
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/shared/types/seat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface SeatData {
created_at: number;
active_until: number | null;
active_from: number;
organization_id?: string;
organization_name?: string;
Comment thread
charlesvien marked this conversation as resolved.
}

export const SEAT_PRODUCT_KEY = "posthog_code";
Expand Down
Loading