Skip to content

Commit cf0694f

Browse files
committed
Move seat fetching to auth session hook and use async client
1 parent 7464ba3 commit cf0694f

4 files changed

Lines changed: 149 additions & 98 deletions

File tree

apps/code/src/renderer/features/auth/hooks/useAuthSession.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useCurrentUser,
99
} from "@features/auth/hooks/authQueries";
1010
import { useAuthUiStateStore } from "@features/auth/stores/authUiStateStore";
11+
import { useSeatStore } from "@features/billing/stores/seatStore";
1112
import { trpcClient } from "@renderer/trpc/client";
1213
import { identifyUser, resetUser } from "@utils/analytics";
1314
import { logger } from "@utils/logger";
@@ -80,6 +81,17 @@ function useAuthAnalyticsIdentity(
8081
}, [authIdentity, authState.cloudRegion, authState.projectId, currentUser]);
8182
}
8283

84+
function useSeatSync(authIdentity: string | null): void {
85+
useEffect(() => {
86+
if (!authIdentity) {
87+
useSeatStore.getState().reset();
88+
return;
89+
}
90+
91+
void useSeatStore.getState().fetchSeat();
92+
}, [authIdentity]);
93+
}
94+
8395
export function useAuthSession() {
8496
const authState = useAuthStateValue((state) => state);
8597
const client = useOptionalAuthenticatedClient();
@@ -89,6 +101,7 @@ export function useAuthSession() {
89101
useAuthSubscriptionSync();
90102
useAuthIdentitySync(authIdentity, authState.cloudRegion);
91103
useAuthAnalyticsIdentity(authIdentity, authState, currentUser);
104+
useSeatSync(authIdentity);
92105

93106
return {
94107
authState,
Lines changed: 95 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { useAuthStore } from "@features/auth/stores/authStore";
1+
import { getAuthenticatedClient } from "@features/auth/hooks/authClient";
22
import type { SeatData } from "@shared/types/seat";
33
import { PLAN_FREE, PLAN_PRO } from "@shared/types/seat";
4-
import { electronStorage } from "@utils/electronStorage";
54
import { logger } from "@utils/logger";
65
import { getPostHogUrl } from "@utils/urls";
76
import { create } from "zustand";
8-
import { persist } from "zustand/middleware";
97

108
const log = logger.scope("seat-store");
119

@@ -28,8 +26,8 @@ interface SeatStoreActions {
2826

2927
type SeatStore = SeatStoreState & SeatStoreActions;
3028

31-
function getClient() {
32-
const client = useAuthStore.getState().client;
29+
async function getClient() {
30+
const client = await getAuthenticatedClient();
3331
if (!client) {
3432
throw new Error("Not authenticated");
3533
}
@@ -115,94 +113,96 @@ const initialState: SeatStoreState = {
115113
redirectUrl: null,
116114
};
117115

118-
export const useSeatStore = create<SeatStore>()(
119-
persist(
120-
(set) => ({
121-
...initialState,
122-
123-
fetchSeat: async () => {
124-
set({ isLoading: true, error: null, redirectUrl: null });
125-
try {
126-
const client = getClient();
127-
let seat = await client.getMySeat();
128-
if (!seat) {
129-
log.info("No seat found, auto-provisioning free plan");
130-
seat = await client.createSeat(PLAN_FREE);
131-
}
132-
set({ seat, isLoading: false });
133-
} catch (error) {
134-
handleSeatError(error, set);
135-
}
136-
},
137-
138-
provisionFreeSeat: async () => {
139-
set({ isLoading: true, error: null, redirectUrl: null });
140-
try {
141-
const client = getClient();
142-
const existing = await client.getMySeat();
143-
if (existing) {
144-
set({ seat: existing, isLoading: false });
145-
return;
146-
}
147-
const seat = await client.createSeat(PLAN_FREE);
148-
set({ seat, isLoading: false });
149-
} catch (error) {
150-
handleSeatError(error, set);
151-
}
152-
},
153-
154-
upgradeToPro: async () => {
155-
set({ isLoading: true, error: null, redirectUrl: null });
156-
try {
157-
const client = getClient();
158-
const existing = await client.getMySeat();
159-
if (existing) {
160-
if (existing.plan_key === PLAN_PRO) {
161-
set({ seat: existing, isLoading: false });
162-
return;
163-
}
164-
const seat = await client.upgradeSeat(PLAN_PRO);
165-
set({ seat, isLoading: false });
166-
return;
167-
}
168-
const seat = await client.createSeat(PLAN_PRO);
169-
set({ seat, isLoading: false });
170-
} catch (error) {
171-
handleSeatError(error, set);
172-
}
173-
},
174-
175-
cancelSeat: async () => {
176-
set({ isLoading: true, error: null, redirectUrl: null });
177-
try {
178-
const client = getClient();
179-
await client.cancelSeat();
180-
const seat = await client.getMySeat();
181-
set({ seat, isLoading: false });
182-
} catch (error) {
183-
handleSeatError(error, set);
184-
}
185-
},
186-
187-
reactivateSeat: async () => {
188-
set({ isLoading: true, error: null, redirectUrl: null });
189-
try {
190-
const client = getClient();
191-
const seat = await client.reactivateSeat();
192-
set({ seat, isLoading: false });
193-
} catch (error) {
194-
handleSeatError(error, set);
116+
export const useSeatStore = create<SeatStore>()((set) => ({
117+
...initialState,
118+
119+
fetchSeat: async () => {
120+
set({ isLoading: true, error: null, redirectUrl: null });
121+
try {
122+
const client = await getClient();
123+
let seat = await client.getMySeat();
124+
if (!seat) {
125+
log.info("No seat found, auto-provisioning free plan");
126+
seat = await client.createSeat(PLAN_FREE);
127+
}
128+
set({ seat, isLoading: false });
129+
} catch (error) {
130+
handleSeatError(error, set);
131+
}
132+
},
133+
134+
provisionFreeSeat: async () => {
135+
log.info("[seat] provisionFreeSeat called");
136+
set({ isLoading: true, error: null, redirectUrl: null });
137+
try {
138+
const client = await getClient();
139+
const existing = await client.getMySeat();
140+
if (existing) {
141+
log.info("[seat] seat already exists on server", {
142+
plan: existing.plan_key,
143+
status: existing.status,
144+
});
145+
set({ seat: existing, isLoading: false });
146+
return;
147+
}
148+
log.info("[seat] creating free seat");
149+
const seat = await client.createSeat(PLAN_FREE);
150+
log.info("[seat] free seat created", {
151+
id: seat.id,
152+
plan: seat.plan_key,
153+
});
154+
set({ seat, isLoading: false });
155+
} catch (error) {
156+
log.error("[seat] provisionFreeSeat failed", error);
157+
handleSeatError(error, set);
158+
}
159+
},
160+
161+
upgradeToPro: async () => {
162+
set({ isLoading: true, error: null, redirectUrl: null });
163+
try {
164+
const client = await getClient();
165+
const existing = await client.getMySeat();
166+
if (existing) {
167+
if (existing.plan_key === PLAN_PRO) {
168+
set({ seat: existing, isLoading: false });
169+
return;
195170
}
196-
},
197-
198-
clearError: () => set({ error: null, redirectUrl: null }),
199-
200-
reset: () => set(initialState),
201-
}),
202-
{
203-
name: "posthog-code-seat",
204-
storage: electronStorage,
205-
partialize: (state) => ({ seat: state.seat }),
206-
},
207-
),
208-
);
171+
const seat = await client.upgradeSeat(PLAN_PRO);
172+
set({ seat, isLoading: false });
173+
return;
174+
}
175+
const seat = await client.createSeat(PLAN_PRO);
176+
set({ seat, isLoading: false });
177+
} catch (error) {
178+
handleSeatError(error, set);
179+
}
180+
},
181+
182+
cancelSeat: async () => {
183+
set({ isLoading: true, error: null, redirectUrl: null });
184+
try {
185+
const client = await getClient();
186+
await client.cancelSeat();
187+
const seat = await client.getMySeat();
188+
set({ seat, isLoading: false });
189+
} catch (error) {
190+
handleSeatError(error, set);
191+
}
192+
},
193+
194+
reactivateSeat: async () => {
195+
set({ isLoading: true, error: null, redirectUrl: null });
196+
try {
197+
const client = await getClient();
198+
const seat = await client.reactivateSeat();
199+
set({ seat, isLoading: false });
200+
} catch (error) {
201+
handleSeatError(error, set);
202+
}
203+
},
204+
205+
clearError: () => set({ error: null, redirectUrl: null }),
206+
207+
reset: () => set(initialState),
208+
}));

apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { useSeatStore } from "@features/billing/stores/seatStore";
2+
import { isFeatureFlagEnabled } from "@utils/analytics";
3+
import { logger } from "@utils/logger";
14
import { create } from "zustand";
25
import { persist } from "zustand/middleware";
36
import type { OnboardingStep } from "../types";
47

8+
const log = logger.scope("onboarding-store");
9+
510
interface OnboardingStoreState {
611
currentStep: OnboardingStep;
712
hasCompletedOnboarding: boolean;
@@ -39,7 +44,30 @@ export const useOnboardingStore = create<OnboardingStore>()(
3944
...initialState,
4045

4146
setCurrentStep: (step) => set({ currentStep: step }),
42-
completeOnboarding: () => set({ hasCompletedOnboarding: true }),
47+
completeOnboarding: () => {
48+
const billingEnabled = isFeatureFlagEnabled("posthog-code-billing");
49+
const existingSeat = useSeatStore.getState().seat;
50+
log.info("[seat] completeOnboarding", {
51+
billingEnabled,
52+
hasSeat: !!existingSeat,
53+
seatPlan: existingSeat?.plan_key ?? null,
54+
});
55+
set({ hasCompletedOnboarding: true });
56+
57+
if (!billingEnabled) {
58+
log.info("[seat] skipped — billing flag disabled");
59+
return;
60+
}
61+
if (existingSeat) {
62+
log.info("[seat] skipped — seat already exists", {
63+
plan: existingSeat.plan_key,
64+
status: existingSeat.status,
65+
});
66+
return;
67+
}
68+
log.info("[seat] no seat found — provisioning free seat");
69+
useSeatStore.getState().provisionFreeSeat();
70+
},
4371
resetOnboarding: () => set({ ...initialState }),
4472
resetSelections: () =>
4573
set({

apps/code/src/renderer/features/settings/components/SettingsDialog.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type SettingsCategory,
44
useSettingsDialogStore,
55
} from "@features/settings/stores/settingsDialogStore";
6+
import { useFeatureFlag } from "@hooks/useFeatureFlag";
67
import { useSeat } from "@hooks/useSeat";
78
import {
89
ArrowLeft,
@@ -24,7 +25,7 @@ import {
2425
} from "@phosphor-icons/react";
2526
import { Avatar, Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
2627
import { useQuery } from "@tanstack/react-query";
27-
import { type ReactNode, useEffect } from "react";
28+
import { type ReactNode, useEffect, useMemo } from "react";
2829
import { useHotkeys } from "react-hotkeys-hook";
2930
import { AdvancedSettings } from "./sections/AdvancedSettings";
3031
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
@@ -119,6 +120,15 @@ export function SettingsDialog() {
119120
useSettingsDialogStore();
120121
const { client, isAuthenticated } = useAuthStore();
121122
const { seat, planLabel } = useSeat();
123+
const billingEnabled = useFeatureFlag("posthog-code-billing");
124+
125+
const sidebarItems = useMemo(
126+
() =>
127+
billingEnabled
128+
? SIDEBAR_ITEMS
129+
: SIDEBAR_ITEMS.filter((item) => item.id !== "plan-usage"),
130+
[billingEnabled],
131+
);
122132

123133
const { data: user } = useQuery({
124134
queryKey: ["currentUser"],
@@ -208,7 +218,7 @@ export function SettingsDialog() {
208218

209219
<ScrollArea style={{ flex: 1 }}>
210220
<div className="flex flex-col pt-2">
211-
{SIDEBAR_ITEMS.map((item) => (
221+
{sidebarItems.map((item) => (
212222
<SidebarNavItem
213223
key={item.id}
214224
item={item}

0 commit comments

Comments
 (0)