Skip to content

Commit c5c3eba

Browse files
committed
settings refactor for plans
1 parent ba913d2 commit c5c3eba

6 files changed

Lines changed: 533 additions & 196 deletions

File tree

apps/code/src/renderer/features/billing/stores/seatStore.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useAuthStore } from "@features/auth/stores/authStore";
2+
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
23
import type { SeatData } from "@shared/types/seat";
34
import { PLAN_FREE, PLAN_PRO } from "@shared/types/seat";
45
import { electronStorage } from "@utils/electronStorage";
@@ -50,6 +51,12 @@ function parseFetcherError(
5051
}
5152
}
5253

54+
function getBillingUrl(): string {
55+
const region = useAuthStore.getState().cloudRegion;
56+
const base = region ? getCloudUrlFromRegion(region) : "http://localhost:8010";
57+
return `${base}/organization/billing`;
58+
}
59+
5360
function handleSeatError(
5461
error: unknown,
5562
set: (state: Partial<SeatStoreState>) => void,
@@ -60,14 +67,16 @@ function handleSeatError(
6067
return;
6168
}
6269

70+
const billingUrl = getBillingUrl();
71+
6372
if (
6473
"redirectUrl" in error &&
6574
typeof (error as { redirectUrl: unknown }).redirectUrl === "string"
6675
) {
6776
set({
6877
isLoading: false,
6978
error: "Billing subscription required",
70-
redirectUrl: (error as { redirectUrl: string }).redirectUrl,
79+
redirectUrl: billingUrl,
7180
});
7281
return;
7382
}
@@ -81,7 +90,7 @@ function handleSeatError(
8190
typeof parsed.body.error === "string"
8291
? parsed.body.error
8392
: "Billing subscription required",
84-
redirectUrl: parsed.body.redirect_url,
93+
redirectUrl: billingUrl,
8594
});
8695
return;
8796
}
@@ -130,12 +139,7 @@ export const useSeatStore = create<SeatStore>()(
130139
const client = getClient();
131140
const existing = await client.getMySeat();
132141
if (existing) {
133-
if (existing.plan_key === PLAN_FREE) {
134-
set({ seat: existing, isLoading: false });
135-
return;
136-
}
137-
const seat = await client.upgradeSeat(PLAN_FREE);
138-
set({ seat, isLoading: false });
142+
set({ seat: existing, isLoading: false });
139143
return;
140144
}
141145
const seat = await client.createSeat(PLAN_FREE);

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

Lines changed: 130 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
1+
import { useAuthStore } from "@features/auth/stores/authStore";
12
import {
23
type SettingsCategory,
34
useSettingsDialogStore,
45
} from "@features/settings/stores/settingsDialogStore";
6+
import { useSeat } from "@hooks/useSeat";
57
import {
68
ArrowLeft,
79
ArrowsClockwise,
810
CaretRight,
911
Code,
12+
CreditCard,
1013
Folder,
1114
GearSix,
1215
HardDrives,
1316
Keyboard,
1417
Palette,
1518
Plugs,
19+
SignOut,
1620
TrafficSignal,
1721
TreeStructure,
18-
User,
1922
Wrench,
2023
} from "@phosphor-icons/react";
21-
import { Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
24+
import { Avatar, Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
25+
import { useQuery } from "@tanstack/react-query";
2226
import { type ReactNode, useEffect } from "react";
2327
import { useHotkeys } from "react-hotkeys-hook";
24-
import { AccountSettings } from "./sections/AccountSettings";
2528
import { AdvancedSettings } from "./sections/AdvancedSettings";
2629
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
2730
import { EnvironmentsSettings } from "./sections/environments/EnvironmentsSettings";
2831
import { GeneralSettings } from "./sections/GeneralSettings";
2932
import { McpServersSettings } from "./sections/McpServersSettings";
3033
import { PersonalizationSettings } from "./sections/PersonalizationSettings";
34+
import { PlanUsageSettings } from "./sections/PlanUsageSettings";
3135
import { ShortcutsSettings } from "./sections/ShortcutsSettings";
3236
import { SignalSourcesSettings } from "./sections/SignalSourcesSettings";
3337
import { UpdatesSettings } from "./sections/UpdatesSettings";
@@ -43,7 +47,7 @@ interface SidebarItem {
4347

4448
const SIDEBAR_ITEMS: SidebarItem[] = [
4549
{ id: "general", label: "General", icon: <GearSix size={16} /> },
46-
{ id: "account", label: "Account", icon: <User size={16} /> },
50+
{ id: "plan-usage", label: "Plan & Usage", icon: <CreditCard size={16} /> },
4751
{ id: "workspaces", label: "Workspaces", icon: <Folder size={16} /> },
4852
{ id: "worktrees", label: "Worktrees", icon: <TreeStructure size={16} /> },
4953
{
@@ -71,7 +75,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
7175

7276
const CATEGORY_TITLES: Record<SettingsCategory, string> = {
7377
general: "General",
74-
account: "Account",
78+
"plan-usage": "Plan & Usage",
7579
workspaces: "Workspaces",
7680
worktrees: "Worktrees",
7781
environments: "Environments",
@@ -87,7 +91,7 @@ const CATEGORY_TITLES: Record<SettingsCategory, string> = {
8791

8892
const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
8993
general: GeneralSettings,
90-
account: AccountSettings,
94+
"plan-usage": PlanUsageSettings,
9195
workspaces: WorkspacesSettings,
9296
worktrees: WorktreesSettings,
9397
environments: EnvironmentsSettings,
@@ -104,6 +108,17 @@ const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
104108
export function SettingsDialog() {
105109
const { isOpen, activeCategory, close, setCategory } =
106110
useSettingsDialogStore();
111+
const { client, isAuthenticated } = useAuthStore();
112+
const { seat, planLabel } = useSeat();
113+
114+
const { data: user } = useQuery({
115+
queryKey: ["currentUser"],
116+
queryFn: async () => {
117+
if (!client) return null;
118+
return await client.getCurrentUser();
119+
},
120+
enabled: !!client && isAuthenticated,
121+
});
107122

108123
useHotkeys("escape", close, {
109124
enabled: isOpen,
@@ -129,13 +144,50 @@ export function SettingsDialog() {
129144

130145
const ActiveComponent = CATEGORY_COMPONENTS[activeCategory];
131146

147+
const initials = user
148+
? user.first_name && user.last_name
149+
? `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
150+
: (user.email?.substring(0, 2).toUpperCase() ?? "U")
151+
: null;
152+
132153
return (
133154
<div
134155
className="fixed inset-0 z-[100] flex"
135156
style={{ backgroundColor: "var(--color-background)" }}
136157
data-overlay="settings"
137158
>
138-
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r pt-8">
159+
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r">
160+
<div
161+
className="drag"
162+
style={{
163+
height: 36,
164+
flexShrink: 0,
165+
borderBottom: "1px solid var(--gray-6)",
166+
}}
167+
/>
168+
169+
{isAuthenticated && user && initials && (
170+
<Flex
171+
align="center"
172+
gap="3"
173+
px="3"
174+
py="3"
175+
style={{ borderBottom: "1px solid var(--gray-5)" }}
176+
>
177+
<Avatar size="2" fallback={initials} radius="full" color="amber" />
178+
<Flex direction="column" style={{ minWidth: 0 }}>
179+
<Text size="2" weight="medium" truncate>
180+
{user.email}
181+
</Text>
182+
{seat && (
183+
<Text size="1" style={{ color: "var(--gray-9)" }}>
184+
{planLabel} Plan
185+
</Text>
186+
)}
187+
</Flex>
188+
</Flex>
189+
)}
190+
139191
<button
140192
type="button"
141193
className="mt-2 flex cursor-pointer items-center gap-2 border-0 bg-transparent px-3 py-2 text-left font-mono text-[12px] text-gray-11 transition-colors hover:bg-gray-3"
@@ -157,56 +209,81 @@ export function SettingsDialog() {
157209
))}
158210
</div>
159211
</ScrollArea>
212+
213+
{isAuthenticated && (
214+
<button
215+
type="button"
216+
className="flex cursor-pointer items-center gap-2 border-0 border-gray-5 border-t bg-transparent px-3 py-2.5 text-left font-mono text-[12px] text-gray-9 transition-colors hover:bg-gray-3 hover:text-gray-11"
217+
onClick={() => useAuthStore.getState().logout()}
218+
>
219+
<SignOut size={14} />
220+
<span>Sign out</span>
221+
</button>
222+
)}
160223
</div>
161224

162-
<div className="relative flex flex-1 justify-center overflow-hidden pt-8">
163-
<svg
164-
aria-hidden="true"
165-
style={{
166-
position: "absolute",
167-
bottom: 0,
168-
left: 0,
169-
width: "100%",
170-
height: "100%",
171-
pointerEvents: "none",
172-
opacity: 0.4,
173-
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
174-
WebkitMaskImage:
175-
"linear-gradient(to top, black 0%, transparent 100%)",
176-
}}
177-
>
178-
<defs>
179-
<pattern
180-
id="settings-dot-pattern"
181-
patternUnits="userSpaceOnUse"
182-
width="8"
183-
height="8"
184-
>
185-
<circle cx="0" cy="0" r="1" fill="var(--gray-6)" />
186-
<circle cx="0" cy="8" r="1" fill="var(--gray-6)" />
187-
<circle cx="8" cy="8" r="1" fill="var(--gray-6)" />
188-
<circle cx="8" cy="0" r="1" fill="var(--gray-6)" />
189-
<circle cx="4" cy="4" r="1" fill="var(--gray-6)" />
190-
</pattern>
191-
</defs>
192-
<rect width="100%" height="100%" fill="url(#settings-dot-pattern)" />
193-
</svg>
194-
<ScrollArea
225+
<div className="relative flex flex-1 flex-col overflow-hidden">
226+
<div
227+
className="drag"
195228
style={{
196-
height: "100%",
197-
width: "100%",
198-
maxWidth: "800px",
229+
height: 36,
230+
flexShrink: 0,
231+
borderBottom: "1px solid var(--gray-6)",
199232
}}
200-
>
201-
<Box p="6" style={{ position: "relative", zIndex: 1 }}>
202-
<Flex direction="column" gap="4">
203-
<Text size="4" weight="medium">
204-
{CATEGORY_TITLES[activeCategory]}
205-
</Text>
206-
<ActiveComponent />
207-
</Flex>
208-
</Box>
209-
</ScrollArea>
233+
/>
234+
<div className="relative flex flex-1 justify-center overflow-hidden">
235+
<svg
236+
aria-hidden="true"
237+
style={{
238+
position: "absolute",
239+
bottom: 0,
240+
left: 0,
241+
width: "100%",
242+
height: "100%",
243+
pointerEvents: "none",
244+
opacity: 0.4,
245+
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
246+
WebkitMaskImage:
247+
"linear-gradient(to top, black 0%, transparent 100%)",
248+
}}
249+
>
250+
<defs>
251+
<pattern
252+
id="settings-dot-pattern"
253+
patternUnits="userSpaceOnUse"
254+
width="8"
255+
height="8"
256+
>
257+
<circle cx="0" cy="0" r="1" fill="var(--gray-6)" />
258+
<circle cx="0" cy="8" r="1" fill="var(--gray-6)" />
259+
<circle cx="8" cy="8" r="1" fill="var(--gray-6)" />
260+
<circle cx="8" cy="0" r="1" fill="var(--gray-6)" />
261+
<circle cx="4" cy="4" r="1" fill="var(--gray-6)" />
262+
</pattern>
263+
</defs>
264+
<rect
265+
width="100%"
266+
height="100%"
267+
fill="url(#settings-dot-pattern)"
268+
/>
269+
</svg>
270+
<ScrollArea
271+
style={{
272+
height: "100%",
273+
width: "100%",
274+
maxWidth: "800px",
275+
}}
276+
>
277+
<Box p="6" style={{ position: "relative", zIndex: 1 }}>
278+
<Flex direction="column" gap="4">
279+
<Text size="4" weight="medium">
280+
{CATEGORY_TITLES[activeCategory]}
281+
</Text>
282+
<ActiveComponent />
283+
</Flex>
284+
</Box>
285+
</ScrollArea>
286+
</div>
210287
</div>
211288
</div>
212289
);

0 commit comments

Comments
 (0)