Skip to content

Commit 016c1a8

Browse files
committed
feat(cloud): billing
1 parent 425131b commit 016c1a8

24 files changed

+885
-75
lines changed

frontend/src/app.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,21 @@ function CloudApp() {
5656
return (
5757
<ClerkProvider
5858
Clerk={clerk}
59-
appearance={{ baseTheme: dark }}
59+
appearance={{
60+
baseTheme: dark,
61+
variables: {
62+
colorPrimary: "hsl(var(--primary))",
63+
colorPrimaryForeground: "hsl(var(--primary-foreground))",
64+
colorTextOnPrimaryBackground:
65+
"hsl(var(--primary-foreground))",
66+
colorBackground: "hsl(var(--background))",
67+
colorInput: "hsl(var(--input))",
68+
colorText: "hsl(var(--text))",
69+
colorTextSecondary: "hsl(var(--muted-foreground))",
70+
borderRadius: "var(--radius)",
71+
colorModalBackdrop: "rgb(0 0 0 / 0.8)",
72+
},
73+
}}
6074
publishableKey={cloudEnv().VITE_CLERK_PUBLISHABLE_KEY}
6175
>
6276
<RouterProvider router={router} />
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { faCheck, faPlus, Icon, type IconProp } from "@rivet-gg/icons";
2+
import type { ReactNode } from "react";
3+
import { Button, cn } from "@/components";
4+
5+
type PlanCardProps = {
6+
title: string;
7+
price: string;
8+
features: { icon: IconProp; label: ReactNode }[];
9+
usageBased?: boolean;
10+
custom?: boolean;
11+
current?: boolean;
12+
buttonProps?: React.ComponentProps<typeof Button>;
13+
} & React.ComponentProps<"div">;
14+
15+
function PlanCard({
16+
title,
17+
price,
18+
features,
19+
usageBased,
20+
current,
21+
custom,
22+
className,
23+
buttonProps,
24+
...props
25+
}: PlanCardProps) {
26+
return (
27+
<div
28+
className={cn(
29+
"border rounded-lg p-6 h-full flex flex-col hover:bg-secondary/20 transition-colors",
30+
current && "border-primary",
31+
className,
32+
)}
33+
{...props}
34+
>
35+
<h3 className="text-lg font-medium mb-2">{title}</h3>
36+
<div className="min-h-24">
37+
{usageBased ? (
38+
<p className="text-xs text-muted-foreground">From</p>
39+
) : null}
40+
<p className="">
41+
<span className="text-4xl font-bold">{price}</span>
42+
{custom ? null : (
43+
<span className="text-muted-foreground ml-1">/mo</span>
44+
)}
45+
</p>
46+
{usageBased ? (
47+
<p className="text-sm text-muted-foreground">+ Usage</p>
48+
) : null}
49+
</div>
50+
<div className="text-sm text-primary-foreground border-t pt-2 flex-1">
51+
<p>Includes:</p>
52+
<ul className="text-muted-foreground mt-2 space-y-1">
53+
{features?.map((feature, index) => (
54+
<li key={feature.label}>
55+
<Icon icon={feature.icon} /> {feature.label}
56+
</li>
57+
))}
58+
</ul>
59+
</div>
60+
{current ? (
61+
<Button
62+
variant="secondary"
63+
className="w-full mt-4"
64+
children="Current Plan"
65+
{...buttonProps}
66+
>
67+
</Button>
68+
) : (
69+
<Button className="w-full mt-4" children={<>{custom ? "Contact Us" : "Upgrade"}</>} {...buttonProps}/>
70+
)}
71+
</div>
72+
);
73+
}
74+
75+
export const CommunityPlan = (props: Partial<PlanCardProps>) => {
76+
return (
77+
<PlanCard
78+
title="Free"
79+
price="$0"
80+
features={[
81+
{ icon: faCheck, label: "5GB Limit" },
82+
{ icon: faCheck, label: "5 Million Writes /mo" },
83+
{ icon: faCheck, label: "200 Million Reads /mo" },
84+
{ icon: faCheck, label: "Community Support" },
85+
]}
86+
{...props}
87+
/>
88+
);
89+
};
90+
91+
export const ProPlan = (props: Partial<PlanCardProps>) => {
92+
return (
93+
<PlanCard
94+
title="Hobby"
95+
price="$5"
96+
usageBased
97+
features={[
98+
{
99+
icon: faPlus,
100+
label: "20 Billion Read /mo",
101+
},
102+
{
103+
icon: faPlus,
104+
label: "50 Million Read /mo",
105+
},
106+
{
107+
icon: faPlus,
108+
label: "5GB Storage",
109+
},
110+
{ icon: faCheck, label: "Unlimited Seats" },
111+
{ icon: faCheck, label: "Email Support" },
112+
]}
113+
{...props}
114+
/>
115+
);
116+
};
117+
118+
export const TeamPlan = (props: Partial<PlanCardProps>) => {
119+
return (
120+
<PlanCard
121+
title="Team"
122+
price="$200"
123+
usageBased
124+
features={[
125+
{ icon: faPlus, label: "25 Billion Reads /mo" },
126+
{ icon: faPlus, label: "50 Million Writes /mo" },
127+
{ icon: faPlus, label: "5GB Storage" },
128+
{ icon: faCheck, label: "Unlimited Seats" },
129+
{ icon: faCheck, label: "MFA" },
130+
{ icon: faCheck, label: "Slack Support" },
131+
]}
132+
{...props}
133+
/>
134+
);
135+
};
136+
137+
export const EnterprisePlan = (props: Partial<PlanCardProps>) => {
138+
return (
139+
<PlanCard
140+
title="Enterprise"
141+
price="Custom"
142+
custom
143+
features={[
144+
{ icon: faCheck, label: "Everything in Team" },
145+
{ icon: faCheck, label: "Priority Support" },
146+
{ icon: faCheck, label: "SLA" },
147+
{ icon: faCheck, label: "OIDC SSO provider" },
148+
{ icon: faCheck, label: "On-Prem Deployment" },
149+
{ icon: faCheck, label: "Audit logs" },
150+
{ icon: faCheck, label: "Custom Roles" },
151+
{ icon: faCheck, label: "Device Tracking" },
152+
]}
153+
{...props}
154+
/>
155+
);
156+
};

frontend/src/app/context-switcher.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function Breadcrumbs() {
7676

7777
const matchProject = match({
7878
to: "/orgs/$organization/projects/$project",
79+
fuzzy: true,
7980
});
8081

8182
if (matchProject) {

frontend/src/app/data-providers/cloud-data-provider.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export const createOrganizationContext = ({
201201
const response = await client.projects.create({
202202
displayName: data.displayName,
203203
name: data.nameId,
204-
org: organization,
204+
organizationId: organization,
205205
});
206206

207207
return response;
@@ -236,10 +236,22 @@ export const createProjectContext = ({
236236
displayName: data.displayName,
237237
org: organization,
238238
});
239-
return response.namespace;
239+
return {
240+
id: response.namespace.id,
241+
name: response.namespace.name,
242+
displayName: response.namespace.displayName,
243+
createdAt: new Date(
244+
response.namespace.createdAt,
245+
).toISOString(),
246+
};
240247
},
241248
};
242249
},
250+
currentProjectQueryOptions: () => {
251+
return parent.currentOrgProjectQueryOptions({
252+
project,
253+
});
254+
},
243255
currentProjectNamespacesQueryOptions: () => {
244256
return parent.orgProjectNamespacesQueryOptions({
245257
organization,
@@ -258,6 +270,31 @@ export const createProjectContext = ({
258270
namespace: opts.namespace,
259271
});
260272
},
273+
currentProjectBillingDetailsQueryOptions() {
274+
return queryOptions({
275+
queryKey: [{ organization, project }, "billing-details"],
276+
queryFn: async ({ signal: abortSignal }) => {
277+
const response = await client.billing.details(
278+
project,
279+
{org: organization },
280+
{ abortSignal },
281+
);
282+
return response;
283+
},
284+
});
285+
},
286+
changeCurrentProjectBillingPlanMutationOptions() {
287+
return {
288+
mutationKey: [{ organization, project }, "billing"],
289+
mutationFn: async (data: Rivet.BillingSetPlanRequest) => {
290+
const response = await client.billing.setPlan(project, {
291+
plan: data.plan,
292+
org: organization,
293+
});
294+
return response;
295+
},
296+
};
297+
},
261298
};
262299
};
263300

@@ -278,7 +315,7 @@ export const createNamespaceContext = ({
278315
...parent,
279316
namespace: engineNamespaceName,
280317
namespaceId: engineNamespaceId,
281-
client: createEngineClient(),
318+
client: createEngineClient(cloudEnv().VITE_APP_CLOUD_ENGINE_URL),
282319
}),
283320
namespaceQueryOptions() {
284321
return parent.currentProjectNamespaceQueryOptions({ namespace });

frontend/src/app/data-providers/engine-data-provider.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export type Namespace = {
2323
createdAt: string;
2424
};
2525

26-
export function createClient() {
26+
export function createClient(baseUrl = engineEnv().VITE_APP_API_URL) {
2727
return new RivetClient({
28-
baseUrl: () => engineEnv().VITE_APP_API_URL,
28+
baseUrl: () => baseUrl,
2929
environment: "",
3030
});
3131
}
@@ -108,6 +108,10 @@ export const createNamespaceContext = ({
108108
statusQueryOptions() {
109109
return queryOptions({
110110
...def.statusQueryOptions(),
111+
queryKey: [
112+
{ namespace, namespaceId },
113+
...def.statusQueryOptions().queryKey,
114+
],
111115
enabled: true,
112116
queryFn: async () => {
113117
return true;
@@ -118,6 +122,10 @@ export const createNamespaceContext = ({
118122
return infiniteQueryOptions({
119123
...def.regionsQueryOptions(),
120124
enabled: true,
125+
queryKey: [
126+
{ namespace, namespaceId },
127+
...def.regionsQueryOptions().queryKey,
128+
],
121129
queryFn: async () => {
122130
const data = await client.datacenters.list();
123131
return {
@@ -133,7 +141,10 @@ export const createNamespaceContext = ({
133141
regionQueryOptions(regionId: string | undefined) {
134142
return queryOptions({
135143
...def.regionQueryOptions(regionId),
136-
queryKey: ["region", regionId],
144+
queryKey: [
145+
{ namespace, namespaceId },
146+
...def.regionQueryOptions(regionId).queryKey,
147+
],
137148
queryFn: async ({ client }) => {
138149
const regions = await client.ensureInfiniteQueryData(
139150
this.regionsQueryOptions(),
@@ -154,7 +165,10 @@ export const createNamespaceContext = ({
154165
actorQueryOptions(actorId) {
155166
return queryOptions({
156167
...def.actorQueryOptions(actorId),
157-
queryKey: [namespace, "actor", actorId],
168+
queryKey: [
169+
{ namespace, namespaceId },
170+
...def.actorQueryOptions(actorId).queryKey,
171+
],
158172
enabled: true,
159173
queryFn: async ({ signal: abortSignal }) => {
160174
const data = await client.actorsGet(
@@ -170,7 +184,10 @@ export const createNamespaceContext = ({
170184
actorsQueryOptions(opts) {
171185
return infiniteQueryOptions({
172186
...def.actorsQueryOptions(opts),
173-
queryKey: [namespace, "actors", opts],
187+
queryKey: [
188+
{ namespace, namespaceId },
189+
...def.actorsQueryOptions(opts).queryKey,
190+
],
174191
enabled: true,
175192
initialPageParam: undefined,
176193
queryFn: async ({
@@ -237,7 +254,10 @@ export const createNamespaceContext = ({
237254
buildsQueryOptions() {
238255
return infiniteQueryOptions({
239256
...def.buildsQueryOptions(),
240-
queryKey: [namespace, "builds"],
257+
queryKey: [
258+
{ namespace, namespaceId },
259+
...def.buildsQueryOptions().queryKey,
260+
],
241261
enabled: true,
242262
queryFn: async ({ signal: abortSignal, pageParam }) => {
243263
const data = await client.actorsListNames(
@@ -402,16 +422,14 @@ export const createNamespaceContext = ({
402422
initialPageParam: undefined as string | undefined,
403423
queryFn: async ({ signal: abortSignal, pageParam }) => {
404424
const response = await client.namespacesRunnerConfigs.list(
405-
namespaceId,
425+
namespace,
406426
{
407427
cursor: pageParam ?? undefined,
408428
limit: RECORDS_PER_PAGE,
409429
},
410430
{ abortSignal },
411431
);
412432

413-
console.log(response);
414-
415433
return response;
416434
},
417435

0 commit comments

Comments
 (0)