Skip to content

Commit a2ebca0

Browse files
committed
settings refactor for plans
1 parent a22843c commit a2ebca0

6 files changed

Lines changed: 537 additions & 193 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: 132 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,39 @@
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
Cloud,
1012
Code,
13+
CreditCard,
1114
Folder,
1215
GearSix,
1316
HardDrives,
1417
Keyboard,
1518
Palette,
1619
Plugs,
20+
SignOut,
1721
TrafficSignal,
1822
TreeStructure,
19-
User,
2023
Wrench,
2124
} from "@phosphor-icons/react";
22-
import { Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
25+
import { Avatar, Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
26+
import { useQuery } from "@tanstack/react-query";
2327
import { type ReactNode, useEffect } from "react";
2428
import { useHotkeys } from "react-hotkeys-hook";
25-
import { AccountSettings } from "./sections/AccountSettings";
2629
import { AdvancedSettings } from "./sections/AdvancedSettings";
2730
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
2831
import { CloudEnvironmentsSettings } from "./sections/CloudEnvironmentsSettings";
2932
import { EnvironmentsSettings } from "./sections/environments/EnvironmentsSettings";
3033
import { GeneralSettings } from "./sections/GeneralSettings";
3134
import { McpServersSettings } from "./sections/McpServersSettings";
3235
import { PersonalizationSettings } from "./sections/PersonalizationSettings";
36+
import { PlanUsageSettings } from "./sections/PlanUsageSettings";
3337
import { ShortcutsSettings } from "./sections/ShortcutsSettings";
3438
import { SignalSourcesSettings } from "./sections/SignalSourcesSettings";
3539
import { UpdatesSettings } from "./sections/UpdatesSettings";
@@ -45,7 +49,7 @@ interface SidebarItem {
4549

4650
const SIDEBAR_ITEMS: SidebarItem[] = [
4751
{ id: "general", label: "General", icon: <GearSix size={16} /> },
48-
{ id: "account", label: "Account", icon: <User size={16} /> },
52+
{ id: "plan-usage", label: "Plan & Usage", icon: <CreditCard size={16} /> },
4953
{ id: "workspaces", label: "Workspaces", icon: <Folder size={16} /> },
5054
{ id: "worktrees", label: "Worktrees", icon: <TreeStructure size={16} /> },
5155
{
@@ -78,7 +82,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
7882

7983
const CATEGORY_TITLES: Record<SettingsCategory, string> = {
8084
general: "General",
81-
account: "Account",
85+
"plan-usage": "Plan & Usage",
8286
workspaces: "Workspaces",
8387
worktrees: "Worktrees",
8488
environments: "Environments",
@@ -95,7 +99,7 @@ const CATEGORY_TITLES: Record<SettingsCategory, string> = {
9599

96100
const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
97101
general: GeneralSettings,
98-
account: AccountSettings,
102+
"plan-usage": PlanUsageSettings,
99103
workspaces: WorkspacesSettings,
100104
worktrees: WorktreesSettings,
101105
environments: EnvironmentsSettings,
@@ -113,6 +117,17 @@ const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
113117
export function SettingsDialog() {
114118
const { isOpen, activeCategory, close, setCategory } =
115119
useSettingsDialogStore();
120+
const { client, isAuthenticated } = useAuthStore();
121+
const { seat, planLabel } = useSeat();
122+
123+
const { data: user } = useQuery({
124+
queryKey: ["currentUser"],
125+
queryFn: async () => {
126+
if (!client) return null;
127+
return await client.getCurrentUser();
128+
},
129+
enabled: !!client && isAuthenticated,
130+
});
116131

117132
useHotkeys("escape", close, {
118133
enabled: isOpen,
@@ -138,13 +153,50 @@ export function SettingsDialog() {
138153

139154
const ActiveComponent = CATEGORY_COMPONENTS[activeCategory];
140155

156+
const initials = user
157+
? user.first_name && user.last_name
158+
? `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
159+
: (user.email?.substring(0, 2).toUpperCase() ?? "U")
160+
: null;
161+
141162
return (
142163
<div
143164
className="fixed inset-0 z-[100] flex"
144165
style={{ backgroundColor: "var(--color-background)" }}
145166
data-overlay="settings"
146167
>
147-
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r pt-8">
168+
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r">
169+
<div
170+
className="drag"
171+
style={{
172+
height: 36,
173+
flexShrink: 0,
174+
borderBottom: "1px solid var(--gray-6)",
175+
}}
176+
/>
177+
178+
{isAuthenticated && user && initials && (
179+
<Flex
180+
align="center"
181+
gap="3"
182+
px="3"
183+
py="3"
184+
style={{ borderBottom: "1px solid var(--gray-5)" }}
185+
>
186+
<Avatar size="2" fallback={initials} radius="full" color="amber" />
187+
<Flex direction="column" style={{ minWidth: 0 }}>
188+
<Text size="2" weight="medium" truncate>
189+
{user.email}
190+
</Text>
191+
{seat && (
192+
<Text size="1" style={{ color: "var(--gray-9)" }}>
193+
{planLabel} Plan
194+
</Text>
195+
)}
196+
</Flex>
197+
</Flex>
198+
)}
199+
148200
<button
149201
type="button"
150202
className="mt-2 flex cursor-pointer items-center gap-2 border-0 bg-transparent px-3 py-2 text-left text-[13px] text-gray-11 transition-colors hover:bg-gray-3"
@@ -166,59 +218,84 @@ export function SettingsDialog() {
166218
))}
167219
</div>
168220
</ScrollArea>
221+
222+
{isAuthenticated && (
223+
<button
224+
type="button"
225+
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"
226+
onClick={() => useAuthStore.getState().logout()}
227+
>
228+
<SignOut size={14} />
229+
<span>Sign out</span>
230+
</button>
231+
)}
169232
</div>
170233

171-
<div className="relative flex flex-1 justify-center overflow-hidden pt-8">
172-
<svg
173-
aria-hidden="true"
174-
style={{
175-
position: "absolute",
176-
bottom: 0,
177-
left: 0,
178-
width: "100%",
179-
height: "100%",
180-
pointerEvents: "none",
181-
opacity: 0.4,
182-
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
183-
WebkitMaskImage:
184-
"linear-gradient(to top, black 0%, transparent 100%)",
185-
}}
186-
>
187-
<defs>
188-
<pattern
189-
id="settings-dot-pattern"
190-
patternUnits="userSpaceOnUse"
191-
width="8"
192-
height="8"
193-
>
194-
<circle cx="0" cy="0" r="1" fill="var(--gray-6)" />
195-
<circle cx="0" cy="8" r="1" fill="var(--gray-6)" />
196-
<circle cx="8" cy="8" r="1" fill="var(--gray-6)" />
197-
<circle cx="8" cy="0" r="1" fill="var(--gray-6)" />
198-
<circle cx="4" cy="4" r="1" fill="var(--gray-6)" />
199-
</pattern>
200-
</defs>
201-
<rect width="100%" height="100%" fill="url(#settings-dot-pattern)" />
202-
</svg>
203-
<ScrollArea
234+
<div className="relative flex flex-1 flex-col overflow-hidden">
235+
<div
236+
className="drag"
204237
style={{
205-
height: "100%",
206-
width: "100%",
238+
height: 36,
239+
flexShrink: 0,
240+
borderBottom: "1px solid var(--gray-6)",
207241
}}
208-
>
209-
<Box
210-
p="6"
211-
mx="auto"
212-
style={{ position: "relative", zIndex: 1, maxWidth: "800px" }}
242+
/>
243+
<div className="relative flex flex-1 justify-center overflow-hidden">
244+
<svg
245+
aria-hidden="true"
246+
style={{
247+
position: "absolute",
248+
bottom: 0,
249+
left: 0,
250+
width: "100%",
251+
height: "100%",
252+
pointerEvents: "none",
253+
opacity: 0.4,
254+
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
255+
WebkitMaskImage:
256+
"linear-gradient(to top, black 0%, transparent 100%)",
257+
}}
213258
>
214-
<Flex direction="column" gap="4">
215-
<Text size="4" weight="medium">
216-
{CATEGORY_TITLES[activeCategory]}
217-
</Text>
218-
<ActiveComponent />
219-
</Flex>
220-
</Box>
221-
</ScrollArea>
259+
<defs>
260+
<pattern
261+
id="settings-dot-pattern"
262+
patternUnits="userSpaceOnUse"
263+
width="8"
264+
height="8"
265+
>
266+
<circle cx="0" cy="0" r="1" fill="var(--gray-6)" />
267+
<circle cx="0" cy="8" r="1" fill="var(--gray-6)" />
268+
<circle cx="8" cy="8" r="1" fill="var(--gray-6)" />
269+
<circle cx="8" cy="0" r="1" fill="var(--gray-6)" />
270+
<circle cx="4" cy="4" r="1" fill="var(--gray-6)" />
271+
</pattern>
272+
</defs>
273+
<rect
274+
width="100%"
275+
height="100%"
276+
fill="url(#settings-dot-pattern)"
277+
/>
278+
</svg>
279+
<ScrollArea
280+
style={{
281+
height: "100%",
282+
width: "100%",
283+
}}
284+
>
285+
<Box
286+
p="6"
287+
mx="auto"
288+
style={{ position: "relative", zIndex: 1, maxWidth: "800px" }}
289+
>
290+
<Flex direction="column" gap="4">
291+
<Text size="4" weight="medium">
292+
{CATEGORY_TITLES[activeCategory]}
293+
</Text>
294+
<ActiveComponent />
295+
</Flex>
296+
</Box>
297+
</ScrollArea>
298+
</div>
222299
</div>
223300
</div>
224301
);

0 commit comments

Comments
 (0)