Skip to content

Commit 8944b78

Browse files
committed
[TOOL-3446] Dashboard: Revamp account onboarding, Add Team onboarding
1 parent 158a08a commit 8944b78

File tree

63 files changed

+2424
-1240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2424
-1240
lines changed

.changeset/chilly-trams-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
Update `TeamResponse` type

apps/dashboard/.storybook/preview.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Inter as interFont } from "next/font/google";
77
// biome-ignore lint/style/useImportType: <explanation>
88
import React from "react";
99
import { useEffect } from "react";
10+
import { Toaster } from "sonner";
1011
import { Button } from "../src/@/components/ui/button";
1112

1213
const queryClient = new QueryClient();
@@ -16,8 +17,30 @@ const fontSans = interFont({
1617
variable: "--font-sans",
1718
});
1819

20+
const customViewports = {
21+
xs: {
22+
// Regular sized phones (iphone 15 / 15 pro)
23+
name: "iPhone",
24+
styles: {
25+
width: "390px",
26+
height: "844px",
27+
},
28+
},
29+
sm: {
30+
// Larger phones (iphone 15 plus / 15 pro max)
31+
name: "iPhone Plus",
32+
styles: {
33+
width: "430px",
34+
height: "932px",
35+
},
36+
},
37+
};
38+
1939
const preview: Preview = {
2040
parameters: {
41+
viewport: {
42+
viewports: customViewports,
43+
},
2144
controls: {
2245
matchers: {
2346
color: /(background|color)$/i,
@@ -57,13 +80,13 @@ function StoryLayout(props: {
5780

5881
return (
5982
<QueryClientProvider client={queryClient}>
60-
<div className="flex min-h-screen min-w-0 flex-col bg-background text-foreground">
83+
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
6184
<div className="flex justify-end gap-2 border-b p-4">
6285
<Button
6386
onClick={() => setTheme("dark")}
6487
size="sm"
6588
variant={theme === "dark" ? "default" : "outline"}
66-
className="h-auto w-auto rounded-full p-2"
89+
className="h-auto w-auto shrink-0 rounded-full p-2"
6790
>
6891
<MoonIcon className="size-4" />
6992
</Button>
@@ -72,14 +95,20 @@ function StoryLayout(props: {
7295
onClick={() => setTheme("light")}
7396
size="sm"
7497
variant={theme === "light" ? "default" : "outline"}
75-
className="h-auto w-auto rounded-full p-2"
98+
className="h-auto w-auto shrink-0 rounded-full p-2"
7699
>
77100
<SunIcon className="size-4" />
78101
</Button>
79102
</div>
80103

81104
<div className="flex min-w-0 grow flex-col">{props.children}</div>
105+
<ToasterSetup />
82106
</div>
83107
</QueryClientProvider>
84108
);
85109
}
110+
111+
function ToasterSetup() {
112+
const { theme } = useTheme();
113+
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
114+
}

apps/dashboard/src/@/actions/billing.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import "server-only";
44
import { API_SERVER_URL } from "@/constants/env";
5-
import { redirect } from "next/navigation";
65
import { getAuthToken } from "../../app/api/lib/getAuthToken";
76
import type { ProductSKU } from "../lib/billing";
87

@@ -15,7 +14,7 @@ export type RedirectCheckoutOptions = {
1514

1615
export async function redirectToCheckout(
1716
options: RedirectCheckoutOptions,
18-
): Promise<{ status: number }> {
17+
): Promise<{ status: number; url?: string }> {
1918
if (!options.teamSlug) {
2019
return {
2120
status: 400,
@@ -49,15 +48,18 @@ export async function redirectToCheckout(
4948
status: res.status,
5049
};
5150
}
51+
5252
const json = await res.json();
5353
if (!json.result) {
5454
return {
5555
status: 500,
5656
};
5757
}
5858

59-
// redirect to the stripe checkout session
60-
redirect(json.result);
59+
return {
60+
status: 200,
61+
url: json.result as string,
62+
};
6163
}
6264

6365
export type RedirectBillingCheckoutAction = typeof redirectToCheckout;
@@ -69,7 +71,7 @@ export type BillingPortalOptions = {
6971

7072
export async function redirectToBillingPortal(
7173
options: BillingPortalOptions,
72-
): Promise<{ status: number }> {
74+
): Promise<{ status: number; url?: string }> {
7375
if (!options.teamSlug) {
7476
return {
7577
status: 400,
@@ -110,8 +112,10 @@ export async function redirectToBillingPortal(
110112
};
111113
}
112114

113-
// redirect to the stripe billing portal
114-
redirect(json.result);
115+
return {
116+
status: 200,
117+
url: json.result as string,
118+
};
115119
}
116120

117121
export type BillingBillingPortalAction = typeof redirectToBillingPortal;

apps/dashboard/src/@/components/billing.tsx

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"use client";
22

3+
import { useMutation } from "@tanstack/react-query";
4+
import { toast } from "sonner";
35
import type {
46
BillingBillingPortalAction,
57
BillingPortalOptions,
68
RedirectBillingCheckoutAction,
79
RedirectCheckoutOptions,
810
} from "../actions/billing";
11+
import { cn } from "../lib/utils";
12+
import { Spinner } from "./ui/Spinner/Spinner";
913
import { Button, type ButtonProps } from "./ui/button";
1014

1115
type CheckoutButtonProps = Omit<RedirectCheckoutOptions, "redirectUrl"> &
1216
ButtonProps & {
13-
redirectPath: string;
1417
redirectToCheckout: RedirectBillingCheckoutAction;
1518
};
1619

@@ -19,60 +22,108 @@ export function CheckoutButton({
1922
teamSlug,
2023
sku,
2124
metadata,
22-
redirectPath,
2325
children,
2426
redirectToCheckout,
2527
...restProps
2628
}: CheckoutButtonProps) {
29+
const redirectMutation = useMutation({
30+
mutationFn: async () => {
31+
return redirectToCheckout({
32+
teamSlug,
33+
sku,
34+
metadata,
35+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
36+
});
37+
},
38+
});
39+
2740
return (
2841
<Button
2942
{...restProps}
43+
className={cn(restProps.className, "gap-2")}
44+
disabled={redirectMutation.isPending || restProps.disabled}
3045
onClick={async (e) => {
3146
onClick?.(e);
32-
await redirectToCheckout({
33-
teamSlug,
34-
sku,
35-
metadata,
36-
redirectUrl: getRedirectUrl(redirectPath),
47+
redirectMutation.mutate(undefined, {
48+
onSuccess: (res) => {
49+
if (!res.url) {
50+
toast.error("Failed to open checkout page");
51+
return;
52+
}
53+
54+
const tab = window.open(res.url, "_blank");
55+
56+
if (!tab) {
57+
toast.error("Failed to open checkout page");
58+
return;
59+
}
60+
},
61+
onError: () => {
62+
toast.error("Failed to open checkout page");
63+
},
3764
});
3865
}}
3966
>
67+
{redirectMutation.isPending && <Spinner className="size-4" />}
4068
{children}
4169
</Button>
4270
);
4371
}
4472

4573
type BillingPortalButtonProps = Omit<BillingPortalOptions, "redirectUrl"> &
4674
ButtonProps & {
47-
redirectPath: string;
4875
redirectToBillingPortal: BillingBillingPortalAction;
4976
};
5077

5178
export function BillingPortalButton({
5279
onClick,
5380
teamSlug,
54-
redirectPath,
5581
children,
5682
redirectToBillingPortal,
5783
...restProps
5884
}: BillingPortalButtonProps) {
85+
const redirectMutation = useMutation({
86+
mutationFn: async () => {
87+
return redirectToBillingPortal({
88+
teamSlug,
89+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
90+
});
91+
},
92+
});
93+
5994
return (
6095
<Button
6196
{...restProps}
97+
className={cn(restProps.className, "gap-2")}
98+
disabled={redirectMutation.isPending || restProps.disabled}
6299
onClick={async (e) => {
63100
onClick?.(e);
64-
await redirectToBillingPortal({
65-
teamSlug,
66-
redirectUrl: getRedirectUrl(redirectPath),
101+
redirectMutation.mutate(undefined, {
102+
onSuccess(res) {
103+
if (!res.url) {
104+
toast.error("Failed to open billing portal");
105+
return;
106+
}
107+
108+
const tab = window.open(res.url, "_blank");
109+
if (!tab) {
110+
toast.error("Failed to open billing portal");
111+
return;
112+
}
113+
},
114+
onError: () => {
115+
toast.error("Failed to open billing portal");
116+
},
67117
});
68118
}}
69119
>
120+
{redirectMutation.isPending && <Spinner className="size-4" />}
70121
{children}
71122
</Button>
72123
);
73124
}
74125

75-
function getRedirectUrl(path: string) {
126+
function getAbsoluteUrl(path: string) {
76127
const url = new URL(window.location.origin);
77128
url.pathname = path;
78129
return url.toString();

apps/dashboard/src/@/components/blocks/pricing-card.tsx

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"use client";
12
import type { Team } from "@/api/team";
23
import { Badge } from "@/components/ui/badge";
34
import { Button } from "@/components/ui/button";
@@ -31,9 +32,7 @@ type PricingCardProps = {
3132
ctaHint?: string;
3233
highlighted?: boolean;
3334
current?: boolean;
34-
canTrialGrowth?: boolean;
3535
activeTrialEndsAt?: string;
36-
redirectPath: string;
3736
redirectToCheckout: RedirectBillingCheckoutAction;
3837
};
3938

@@ -43,9 +42,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
4342
cta,
4443
highlighted = false,
4544
current = false,
46-
canTrialGrowth = false,
4745
activeTrialEndsAt,
48-
redirectPath,
4946
redirectToCheckout,
5047
}) => {
5148
const plan = TEAM_PLANS[billingPlan];
@@ -88,18 +85,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
8885
<div className="flex flex-col gap-0.5">
8986
<div className="flex items-center gap-2">
9087
<span className="font-semibold text-3xl text-foreground tracking-tight">
91-
{isCustomPrice ? (
92-
plan.price
93-
) : canTrialGrowth ? (
94-
<>
95-
<span className="text-muted-foreground line-through">
96-
${plan.price}
97-
</span>{" "}
98-
$0
99-
</>
100-
) : (
101-
`$${plan.price}`
102-
)}
88+
${plan.price}
10389
</span>
10490

10591
{!isCustomPrice && (
@@ -135,7 +121,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
135121
</div>
136122
</div>
137123

138-
<div className="flex grow flex-col items-start gap-2.5 text-foreground">
124+
<div className="flex grow flex-col items-start gap-2 text-foreground">
139125
{plan.subTitle && (
140126
<p className="font-medium text-foreground">{plan.subTitle}</p>
141127
)}
@@ -152,8 +138,8 @@ export const PricingCard: React.FC<PricingCardProps> = ({
152138
variant={cta.variant || "outline"}
153139
teamSlug={teamSlug}
154140
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
155-
redirectPath={redirectPath}
156141
redirectToCheckout={redirectToCheckout}
142+
className="gap-2"
157143
>
158144
{cta.title}
159145
</CheckoutButton>
@@ -189,7 +175,7 @@ function FeatureItem({ text }: FeatureItemProps) {
189175
const titleStr = Array.isArray(text) ? text[0] : text;
190176

191177
return (
192-
<div className="flex items-center gap-2">
178+
<div className="flex items-center gap-2 text-sm">
193179
<CheckIcon className="size-4 shrink-0 text-green-500" />
194180
{Array.isArray(text) ? (
195181
<div className="flex items-center gap-2">

apps/dashboard/src/@/components/ui/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
1111
<input
1212
type={type}
1313
className={cn(
14-
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-foreground/10 file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
14+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-foreground/10 file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:selection:bg-foreground/20",
1515
className,
1616
)}
1717
ref={ref}

0 commit comments

Comments
 (0)