Skip to content

Commit c653d70

Browse files
authored
[TOOL-3446] Dashboard: Update account onboarding UI, Add Team onboarding, Open stripe links in new tab (#6354)
1 parent 77029a2 commit c653d70

File tree

68 files changed

+2991
-1469
lines changed

Some content is hidden

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

68 files changed

+2991
-1469
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: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
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

9-
export type RedirectCheckoutOptions = {
8+
export type GetBillingCheckoutUrlOptions = {
109
teamSlug: string;
1110
sku: ProductSKU;
1211
redirectUrl: string;
1312
metadata?: Record<string, string>;
1413
};
1514

16-
export async function redirectToCheckout(
17-
options: RedirectCheckoutOptions,
18-
): Promise<{ status: number }> {
15+
export async function getBillingCheckoutUrl(
16+
options: GetBillingCheckoutUrlOptions,
17+
): Promise<{ status: number; url?: string }> {
1918
if (!options.teamSlug) {
2019
return {
2120
status: 400,
@@ -49,27 +48,30 @@ 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

63-
export type RedirectBillingCheckoutAction = typeof redirectToCheckout;
65+
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl;
6466

65-
export type BillingPortalOptions = {
67+
export type GetBillingPortalUrlOptions = {
6668
teamSlug: string | undefined;
6769
redirectUrl: string;
6870
};
6971

70-
export async function redirectToBillingPortal(
71-
options: BillingPortalOptions,
72-
): Promise<{ status: number }> {
72+
export async function getBillingPortalUrl(
73+
options: GetBillingPortalUrlOptions,
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

117-
export type BillingBillingPortalAction = typeof redirectToBillingPortal;
121+
export type GetBillingPortalUrlAction = typeof getBillingPortalUrl;

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

Lines changed: 0 additions & 19 deletions
This file was deleted.

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

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,136 @@
11
"use client";
22

3+
import { useMutation } from "@tanstack/react-query";
4+
import { toast } from "sonner";
35
import type {
4-
BillingBillingPortalAction,
5-
BillingPortalOptions,
6-
RedirectBillingCheckoutAction,
7-
RedirectCheckoutOptions,
6+
GetBillingCheckoutUrlAction,
7+
GetBillingCheckoutUrlOptions,
8+
GetBillingPortalUrlAction,
9+
GetBillingPortalUrlOptions,
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

11-
type CheckoutButtonProps = Omit<RedirectCheckoutOptions, "redirectUrl"> &
12-
ButtonProps & {
13-
redirectPath: string;
14-
redirectToCheckout: RedirectBillingCheckoutAction;
15-
};
15+
type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> & {
16+
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
17+
buttonProps?: Omit<ButtonProps, "children">;
18+
children: React.ReactNode;
19+
};
1620

1721
export function CheckoutButton({
18-
onClick,
1922
teamSlug,
2023
sku,
2124
metadata,
22-
redirectPath,
25+
getBillingCheckoutUrl,
2326
children,
24-
redirectToCheckout,
25-
...restProps
27+
buttonProps,
2628
}: CheckoutButtonProps) {
29+
const getUrlMutation = useMutation({
30+
mutationFn: async () => {
31+
return getBillingCheckoutUrl({
32+
teamSlug,
33+
sku,
34+
metadata,
35+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
36+
});
37+
},
38+
});
39+
40+
const errorMessage = "Failed to open checkout page";
41+
2742
return (
2843
<Button
29-
{...restProps}
44+
{...buttonProps}
45+
className={cn(buttonProps?.className, "gap-2")}
46+
disabled={getUrlMutation.isPending || buttonProps?.disabled}
3047
onClick={async (e) => {
31-
onClick?.(e);
32-
await redirectToCheckout({
33-
teamSlug,
34-
sku,
35-
metadata,
36-
redirectUrl: getRedirectUrl(redirectPath),
48+
buttonProps?.onClick?.(e);
49+
getUrlMutation.mutate(undefined, {
50+
onSuccess: (res) => {
51+
if (!res.url) {
52+
toast.error(errorMessage);
53+
return;
54+
}
55+
56+
const tab = window.open(res.url, "_blank");
57+
58+
if (!tab) {
59+
toast.error(errorMessage);
60+
return;
61+
}
62+
},
63+
onError: () => {
64+
toast.error(errorMessage);
65+
},
3766
});
3867
}}
3968
>
69+
{getUrlMutation.isPending && <Spinner className="size-4" />}
4070
{children}
4171
</Button>
4272
);
4373
}
4474

45-
type BillingPortalButtonProps = Omit<BillingPortalOptions, "redirectUrl"> &
46-
ButtonProps & {
47-
redirectPath: string;
48-
redirectToBillingPortal: BillingBillingPortalAction;
49-
};
75+
type BillingPortalButtonProps = Omit<
76+
GetBillingPortalUrlOptions,
77+
"redirectUrl"
78+
> & {
79+
getBillingPortalUrl: GetBillingPortalUrlAction;
80+
buttonProps?: Omit<ButtonProps, "children">;
81+
children: React.ReactNode;
82+
};
5083

5184
export function BillingPortalButton({
52-
onClick,
5385
teamSlug,
54-
redirectPath,
5586
children,
56-
redirectToBillingPortal,
57-
...restProps
87+
getBillingPortalUrl,
88+
buttonProps,
5889
}: BillingPortalButtonProps) {
90+
const getUrlMutation = useMutation({
91+
mutationFn: async () => {
92+
return getBillingPortalUrl({
93+
teamSlug,
94+
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
95+
});
96+
},
97+
});
98+
99+
const errorMessage = "Failed to open billing portal";
100+
59101
return (
60102
<Button
61-
{...restProps}
103+
{...buttonProps}
104+
className={cn(buttonProps?.className, "gap-2")}
105+
disabled={getUrlMutation.isPending || buttonProps?.disabled}
62106
onClick={async (e) => {
63-
onClick?.(e);
64-
await redirectToBillingPortal({
65-
teamSlug,
66-
redirectUrl: getRedirectUrl(redirectPath),
107+
buttonProps?.onClick?.(e);
108+
getUrlMutation.mutate(undefined, {
109+
onSuccess(res) {
110+
if (!res.url) {
111+
toast.error(errorMessage);
112+
return;
113+
}
114+
115+
const tab = window.open(res.url, "_blank");
116+
if (!tab) {
117+
toast.error(errorMessage);
118+
return;
119+
}
120+
},
121+
onError: () => {
122+
toast.error(errorMessage);
123+
},
67124
});
68125
}}
69126
>
127+
{getUrlMutation.isPending && <Spinner className="size-4" />}
70128
{children}
71129
</Button>
72130
);
73131
}
74132

75-
function getRedirectUrl(path: string) {
133+
function getAbsoluteUrl(path: string) {
76134
const url = new URL(window.location.origin);
77135
url.pathname = path;
78136
return url.toString();

0 commit comments

Comments
 (0)