Skip to content

Commit e9ea476

Browse files
committed
[Dashboard] Feature: Integrate ecosystem creation with Stripe billing (#5488)
### TL;DR Refactored ecosystem creation flow to use server actions and removed the confirmation dialog. ### What changed? - Created a new server action `createEcosystem` to handle ecosystem creation - Extracted `BASE_URL` constant for reusability - Removed the confirmation dialog from the creation form - Simplified form submission to directly create ecosystems - Added more specific error handling with user-friendly toast messages - Changed component props to use `teamSlug` instead of `ecosystemLayoutPath` ### How to test? 1. Navigate to the ecosystem creation page 2. Fill out the ecosystem form with a name and logo 3. Select permission type (PARTNER_WHITELIST or ANYONE) 4. Submit the form 5. Verify redirect to Stripe billing portal 6. Verify appropriate error messages display for various error scenarios ### Why make this change? To streamline the ecosystem creation process by implementing server-side actions and providing better error handling. This change improves the user experience by removing the extra confirmation step while maintaining secure server-side processing of ecosystem creation. <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on refactoring the `EcosystemCreatePage` and related components to streamline the creation of ecosystems, updating function signatures, and improving the handling of the creation process. ### Detailed summary - Removed `ecosystemLayoutPath` prop from `EcosystemCreatePage`. - Added `teamSlug` prop to `EcosystemCreatePage` and `CreateEcosystemForm`. - Updated `createEcosystem` function to handle authentication and API calls. - Enhanced error handling in the `CreateEcosystemForm`. - Removed unused imports and state management related to confirmation dialogs. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent d98c133 commit e9ea476

File tree

6 files changed

+110
-151
lines changed

6 files changed

+110
-151
lines changed

apps/dashboard/src/@/constants/env.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
3434
// Comma-separated list of chain IDs to disable faucet for.
3535
export const DISABLE_FAUCET_CHAIN_IDS = process.env.DISABLE_FAUCET_CHAIN_IDS;
3636

37+
export const BASE_URL = isProd
38+
? "https://thirdweb.com"
39+
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
40+
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
41+
: "http://localhost:3000") || "https://thirdweb-dev.com";
42+
3743
export function getAbsoluteUrlFromPath(path: string) {
38-
const url = new URL(
39-
isProd
40-
? "https://thirdweb.com"
41-
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
42-
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
43-
: "http://localhost:3000") || "https://thirdweb-dev.com",
44-
);
44+
const url = new URL(BASE_URL);
4545

4646
url.pathname = path;
4747
return url;

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/create/EcosystemCreatePage.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client";
22
import { EcosystemWalletPricingCard } from "./components/pricing-card";
33

4-
export function EcosystemCreatePage(props: {
5-
ecosystemLayoutPath: string;
6-
}) {
4+
export async function EcosystemCreatePage(props: { teamSlug: string }) {
75
return (
86
<div className="flex w-full flex-col gap-6 md:mx-auto md:max-w-lg lg:max-w-4xl">
97
<header className="flex flex-col gap-1">
@@ -19,9 +17,7 @@ export function EcosystemCreatePage(props: {
1917
<EcosystemWalletPricingCard />
2018
</section>
2119
<section className="mb-12 lg:px-4">
22-
<CreateEcosystemForm
23-
ecosystemLayoutPath={props.ecosystemLayoutPath}
24-
/>
20+
<CreateEcosystemForm teamSlug={props.teamSlug} />
2521
</section>
2622
</main>
2723
</div>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use server";
2+
import "server-only";
3+
import { API_SERVER_URL, BASE_URL } from "@/constants/env";
4+
import { getThirdwebClient } from "@/constants/thirdweb.server";
5+
import { redirect } from "next/navigation";
6+
import { upload } from "thirdweb/storage";
7+
import { getAuthToken } from "../../../../../../../api/lib/getAuthToken";
8+
9+
export async function createEcosystem(options: {
10+
teamSlug: string;
11+
name: string;
12+
logo: File;
13+
permission: "PARTNER_WHITELIST" | "ANYONE";
14+
}) {
15+
const token = await getAuthToken();
16+
if (!token) {
17+
return {
18+
status: 401,
19+
};
20+
}
21+
22+
const { teamSlug, logo, ...data } = options;
23+
24+
const imageUrl = await upload({
25+
client: getThirdwebClient(token),
26+
files: [logo],
27+
});
28+
29+
const res = await fetch(
30+
`${API_SERVER_URL}/v1/teams/${teamSlug}/checkout/create-link`,
31+
{
32+
method: "POST",
33+
body: JSON.stringify({
34+
baseUrl: BASE_URL,
35+
sku: "product:ecosystem_wallets",
36+
metadata: {
37+
...data,
38+
imageUrl,
39+
// not something we pass in today during creation, but required to be there
40+
authOptions: [],
41+
},
42+
}),
43+
headers: {
44+
"Content-Type": "application/json",
45+
Authorization: `Bearer ${token}`,
46+
},
47+
},
48+
);
49+
if (!res.ok) {
50+
return {
51+
status: res.status,
52+
};
53+
}
54+
55+
const json = await res.json();
56+
57+
if (!json.result) {
58+
return {
59+
status: 500,
60+
};
61+
}
62+
63+
// redirect to the stripe billing portal
64+
redirect(json.result);
65+
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/create/components/client/create-ecosystem-form.client.tsx

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use client";
2-
import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog";
32
import { Button } from "@/components/ui/button";
43
import {
54
Form,
@@ -14,16 +13,13 @@ import {
1413
import { ImageUpload } from "@/components/ui/image-upload";
1514
import { Input } from "@/components/ui/input";
1615
import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group";
17-
import { useDashboardRouter } from "@/lib/DashboardRouter";
1816
import { zodResolver } from "@hookform/resolvers/zod";
1917
import { Loader2 } from "lucide-react";
2018
import Link from "next/link";
21-
import { useState } from "react";
2219
import { useForm } from "react-hook-form";
2320
import { toast } from "sonner";
24-
import invariant from "tiny-invariant";
2521
import { z } from "zod";
26-
import { useCreateEcosystem } from "../../hooks/use-create-ecosystem";
22+
import { createEcosystem } from "../../actions/create-ecosystem";
2723

2824
const formSchema = z.object({
2925
name: z
@@ -40,41 +36,47 @@ const formSchema = z.object({
4036
permission: z.union([z.literal("PARTNER_WHITELIST"), z.literal("ANYONE")]),
4137
});
4238

43-
export function CreateEcosystemForm(props: {
44-
ecosystemLayoutPath: string;
45-
}) {
46-
// When set, the confirmation modal is open the this contains the form data to be submitted
47-
const [formDataToBeConfirmed, setFormDataToBeConfirmed] = useState<
48-
z.infer<typeof formSchema> | undefined
49-
>();
50-
51-
const router = useDashboardRouter();
39+
export function CreateEcosystemForm(props: { teamSlug: string }) {
5240
const form = useForm<z.infer<typeof formSchema>>({
5341
resolver: zodResolver(formSchema),
5442
defaultValues: {
5543
permission: "PARTNER_WHITELIST",
5644
},
5745
});
5846

59-
const { mutateAsync: createEcosystem, isPending } = useCreateEcosystem({
60-
onError: (error) => {
61-
const message =
62-
error instanceof Error ? error.message : "Failed to create ecosystem";
63-
toast.error(message);
64-
},
65-
onSuccess: (slug: string) => {
66-
form.reset();
67-
router.push(`${props.ecosystemLayoutPath}/${slug}`);
68-
},
69-
});
70-
7147
return (
7248
<>
7349
<Form {...form}>
7450
<form
75-
onSubmit={form.handleSubmit((values) =>
76-
setFormDataToBeConfirmed(values),
77-
)}
51+
onSubmit={form.handleSubmit(async (values) => {
52+
const res = await createEcosystem({
53+
teamSlug: props.teamSlug,
54+
...values,
55+
});
56+
switch (res.status) {
57+
case 401: {
58+
toast.error("Please login to create an ecosystem");
59+
break;
60+
}
61+
case 403:
62+
{
63+
toast.error(
64+
"You are not authorized to create an ecosystem, please ask your team owner to create it.",
65+
);
66+
}
67+
break;
68+
case 409: {
69+
toast.error("An ecosystem with that name already exists.");
70+
break;
71+
}
72+
// any other status code treat as a random failure
73+
default: {
74+
toast.error(
75+
"Failed to create ecosystem, please try again later.",
76+
);
77+
}
78+
}
79+
})}
7880
className="flex flex-col items-stretch gap-8"
7981
>
8082
<div className="grid gap-6">
@@ -166,29 +168,15 @@ export function CreateEcosystemForm(props: {
166168
type="submit"
167169
variant="primary"
168170
className="w-full"
169-
disabled={isPending}
171+
disabled={form.formState.isSubmitting}
170172
>
171-
{isPending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
173+
{form.formState.isSubmitting && (
174+
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
175+
)}
172176
Create
173177
</Button>
174178
</form>
175179
</Form>
176-
<ConfirmationDialog
177-
open={formDataToBeConfirmed !== undefined}
178-
onOpenChange={(open) =>
179-
!open ? setFormDataToBeConfirmed(undefined) : null
180-
}
181-
title={`Are you sure you want to create ecosystem ${form.getValues().name}?`}
182-
description="Your account will be charged $250 per month."
183-
onSubmit={() => {
184-
invariant(formDataToBeConfirmed, "Form data not found");
185-
createEcosystem({
186-
name: formDataToBeConfirmed.name,
187-
logo: formDataToBeConfirmed.logo,
188-
permission: formDataToBeConfirmed.permission,
189-
});
190-
}}
191-
/>
192180
</>
193181
);
194182
}

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/create/hooks/use-create-ecosystem.ts

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

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/create/page.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,5 @@ export default async function Page(props: {
44
params: Promise<{ team_slug: string }>;
55
}) {
66
const { team_slug } = await props.params;
7-
return (
8-
<EcosystemCreatePage
9-
ecosystemLayoutPath={`/team/${team_slug}/~/ecosystem`}
10-
/>
11-
);
7+
return <EcosystemCreatePage teamSlug={team_slug} />;
128
}

0 commit comments

Comments
 (0)