Skip to content

Commit 945ac3d

Browse files
committed
Add project wallet integration to payments UI
Introduces a server utility to fetch the project wallet and propagates the project wallet address to payment-related components. Updates payment link creation and payout address forms to allow users to autofill with the project wallet address, improving usability and consistency across the dashboard.
1 parent b48949c commit 945ac3d

File tree

8 files changed

+200
-102
lines changed

8 files changed

+200
-102
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import "server-only";
2+
3+
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
4+
import type { Project } from "@/api/project/projects";
5+
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
6+
import { getProjectWalletLabel } from "@/lib/project-wallet";
7+
8+
export type ProjectWalletSummary = {
9+
id: string;
10+
address: string;
11+
label?: string;
12+
};
13+
14+
type VaultWalletListItem = {
15+
id: string;
16+
address: string;
17+
metadata?: {
18+
label?: string;
19+
projectId?: string;
20+
teamId?: string;
21+
type?: string;
22+
};
23+
};
24+
25+
export async function getProjectWallet(
26+
project: Project,
27+
): Promise<ProjectWalletSummary | undefined> {
28+
const engineCloudService = project.services.find(
29+
(service) => service.name === "engineCloud",
30+
);
31+
32+
const managementAccessToken =
33+
engineCloudService?.managementAccessToken || undefined;
34+
35+
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
36+
return undefined;
37+
}
38+
39+
try {
40+
const vaultClient = await createVaultClient({
41+
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
42+
});
43+
44+
const response = await listEoas({
45+
client: vaultClient,
46+
request: {
47+
auth: {
48+
accessToken: managementAccessToken,
49+
},
50+
options: {
51+
page: 0,
52+
// @ts-expect-error - SDK expects snake_case for pagination arguments
53+
page_size: 25,
54+
},
55+
},
56+
});
57+
58+
if (!response.success || !response.data) {
59+
return undefined;
60+
}
61+
62+
const items = response.data.items as VaultWalletListItem[] | undefined;
63+
64+
if (!items?.length) {
65+
return undefined;
66+
}
67+
68+
const expectedLabel = getProjectWalletLabel(project.name);
69+
70+
const serverWallets = items.filter(
71+
(item) => item.metadata?.projectId === project.id,
72+
);
73+
74+
const defaultWallet =
75+
serverWallets.find((item) => item.metadata?.label === expectedLabel) ??
76+
serverWallets.find((item) => item.metadata?.type === "server-wallet") ??
77+
serverWallets[0];
78+
79+
if (!defaultWallet) {
80+
return undefined;
81+
}
82+
83+
return {
84+
id: defaultWallet.id,
85+
address: defaultWallet.address,
86+
label: defaultWallet.metadata?.label,
87+
};
88+
} catch (error) {
89+
console.error("Failed to load project wallet", error);
90+
return undefined;
91+
}
92+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx

Lines changed: 5 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
21
import {
32
ArrowLeftRightIcon,
43
ChevronRightIcon,
@@ -11,7 +10,6 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
1110
import { CopyTextButton } from "@/components/ui/CopyTextButton";
1211
import { CodeServer } from "@/components/ui/code/code.server";
1312
import { UnderlineLink } from "@/components/ui/UnderlineLink";
14-
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
1513
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
1614
import { GithubIcon } from "@/icons/brand-icons/GithubIcon";
1715
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
@@ -23,21 +21,19 @@ import { InsightIcon } from "@/icons/InsightIcon";
2321
import { PayIcon } from "@/icons/PayIcon";
2422
import { WalletProductIcon } from "@/icons/WalletProductIcon";
2523
import { getProjectWalletLabel } from "@/lib/project-wallet";
24+
import {
25+
getProjectWallet,
26+
type ProjectWalletSummary,
27+
} from "@/lib/server/project-wallet";
2628
import { ClientIDSection } from "./ClientIDSection";
2729
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
2830
import { SecretKeySection } from "./SecretKeySection";
2931

30-
type ProjectWalletSummary = {
31-
id: string;
32-
address: string;
33-
label?: string;
34-
};
35-
3632
export async function ProjectFTUX(props: {
3733
project: Project;
3834
teamSlug: string;
3935
}) {
40-
const projectWallet = await fetchProjectWallet(props.project);
36+
const projectWallet = await getProjectWallet(props.project);
4137

4238
return (
4339
<div className="flex flex-col gap-10">
@@ -139,86 +135,6 @@ function ProjectWalletSection(props: {
139135
);
140136
}
141137

142-
type VaultWalletListItem = {
143-
id: string;
144-
address: string;
145-
metadata?: {
146-
label?: string;
147-
projectId?: string;
148-
teamId?: string;
149-
type?: string;
150-
};
151-
};
152-
153-
async function fetchProjectWallet(
154-
project: Project,
155-
): Promise<ProjectWalletSummary | undefined> {
156-
const engineCloudService = project.services.find(
157-
(service) => service.name === "engineCloud",
158-
);
159-
160-
const managementAccessToken =
161-
engineCloudService?.managementAccessToken || undefined;
162-
163-
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
164-
return undefined;
165-
}
166-
167-
try {
168-
const vaultClient = await createVaultClient({
169-
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
170-
});
171-
172-
const response = await listEoas({
173-
client: vaultClient,
174-
request: {
175-
auth: {
176-
accessToken: managementAccessToken,
177-
},
178-
options: {
179-
page: 0,
180-
// @ts-expect-error - SDK expects snake_case for pagination arguments
181-
page_size: 25,
182-
},
183-
},
184-
});
185-
186-
if (!response.success || !response.data) {
187-
return undefined;
188-
}
189-
190-
const items = response.data.items as VaultWalletListItem[] | undefined;
191-
192-
if (!items?.length) {
193-
return undefined;
194-
}
195-
196-
const expectedLabel = getProjectWalletLabel(project.name);
197-
198-
const serverWallets = items.filter(
199-
(item) => item.metadata?.projectId === project.id,
200-
);
201-
202-
const defaultWallet =
203-
serverWallets.find((item) => item.metadata?.label === expectedLabel) ||
204-
serverWallets.find((item) => item.metadata?.type === "server-wallet") ||
205-
serverWallets[0];
206-
207-
if (!defaultWallet) {
208-
return undefined;
209-
}
210-
211-
return {
212-
id: defaultWallet.id,
213-
address: defaultWallet.address,
214-
label: defaultWallet.metadata?.label,
215-
};
216-
} catch (error) {
217-
console.error("Failed to load project wallet", error);
218-
return undefined;
219-
}
220-
}
221-
222138
// Integrate API key section ------------------------------------------------------------
223139

224140
function IntegrateAPIKeySection({

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/QuickstartSection.client.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function QuickStartSection(props: {
1515
projectSlug: string;
1616
clientId: string;
1717
teamId: string;
18+
projectWalletAddress?: string;
1819
}) {
1920
return (
2021
<section>
@@ -43,6 +44,7 @@ export function QuickStartSection(props: {
4344
action={
4445
<CreatePaymentLinkButton
4546
clientId={props.clientId}
47+
projectWalletAddress={props.projectWalletAddress}
4648
teamId={props.teamId}
4749
>
4850
<Button

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/CreatePaymentLinkButton.client.tsx

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ const formSchema = z.object({
4545
});
4646

4747
export function CreatePaymentLinkButton(
48-
props: PropsWithChildren<{ clientId: string; teamId: string }>,
48+
props: PropsWithChildren<{
49+
clientId: string;
50+
teamId: string;
51+
projectWalletAddress?: string;
52+
}>,
4953
) {
5054
const [open, setOpen] = useState(false);
5155

@@ -55,6 +59,7 @@ export function CreatePaymentLinkButton(
5559
<DialogContent className="p-0 !max-w-lg">
5660
<CreatePaymentLinkDialogContent
5761
clientId={props.clientId}
62+
projectWalletAddress={props.projectWalletAddress}
5863
setOpen={setOpen}
5964
teamId={props.teamId}
6065
/>
@@ -67,6 +72,7 @@ function CreatePaymentLinkDialogContent(props: {
6772
clientId: string;
6873
teamId: string;
6974
setOpen: (open: boolean) => void;
75+
projectWalletAddress?: string;
7076
}) {
7177
const client = getClientThirdwebClient();
7278

@@ -182,14 +188,41 @@ function CreatePaymentLinkDialogContent(props: {
182188
render={({ field }) => (
183189
<FormItem>
184190
<FormLabel>Recipient Address</FormLabel>
185-
<Input
186-
className="w-full bg-card"
187-
{...field}
188-
onChange={field.onChange}
189-
value={field.value}
190-
placeholder="Address or ENS"
191-
required
192-
/>
191+
<div className="flex flex-col gap-2 sm:flex-row">
192+
<Input
193+
className="w-full bg-card sm:flex-1"
194+
{...field}
195+
onChange={field.onChange}
196+
value={field.value}
197+
placeholder="Address or ENS"
198+
required
199+
/>
200+
{props.projectWalletAddress && (
201+
<Button
202+
className="sm:w-auto"
203+
onClick={() => {
204+
if (!props.projectWalletAddress) {
205+
return;
206+
}
207+
208+
form.setValue(
209+
"recipient",
210+
props.projectWalletAddress,
211+
{
212+
shouldDirty: true,
213+
shouldTouch: true,
214+
shouldValidate: true,
215+
},
216+
);
217+
}}
218+
size="sm"
219+
type="button"
220+
variant="outline"
221+
>
222+
Use Project Wallet
223+
</Button>
224+
)}
225+
</div>
193226
<FormMessage />
194227
</FormItem>
195228
)}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/links/components/PaymentLinksTable.client.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ import { ErrorState } from "../../components/ErrorState";
4040
import { formatTokenAmount } from "../../components/format";
4141
import { CreatePaymentLinkButton } from "./CreatePaymentLinkButton.client";
4242

43-
export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
43+
export function PaymentLinksTable(props: {
44+
clientId: string;
45+
teamId: string;
46+
projectWalletAddress?: string;
47+
}) {
4448
return (
4549
<section>
4650
<div className="mb-4">
@@ -49,12 +53,20 @@ export function PaymentLinksTable(props: { clientId: string; teamId: string }) {
4953
The payments you have created in this project
5054
</p>
5155
</div>
52-
<PaymentLinksTableInner clientId={props.clientId} teamId={props.teamId} />
56+
<PaymentLinksTableInner
57+
clientId={props.clientId}
58+
projectWalletAddress={props.projectWalletAddress}
59+
teamId={props.teamId}
60+
/>
5361
</section>
5462
);
5563
}
5664

57-
function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
65+
function PaymentLinksTableInner(props: {
66+
clientId: string;
67+
teamId: string;
68+
projectWalletAddress?: string;
69+
}) {
5870
const paymentLinksQuery = useQuery({
5971
queryFn: async () => {
6072
return getPaymentLinks({
@@ -120,6 +132,7 @@ function PaymentLinksTableInner(props: { clientId: string; teamId: string }) {
120132
<CreatePaymentLinkButton
121133
key="create-payment-link"
122134
clientId={props.clientId}
135+
projectWalletAddress={props.projectWalletAddress}
123136
teamId={props.teamId}
124137
>
125138
<Button className="gap-2 rounded-full" variant="default" size="sm">

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getProject } from "@/api/project/projects";
66
import { ProjectPage } from "@/components/blocks/project-page/project-page";
77
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
88
import { PayIcon } from "@/icons/PayIcon";
9+
import { getProjectWallet } from "@/lib/server/project-wallet";
910
import { loginRedirect } from "@/utils/redirects";
1011
import { AdvancedSection } from "./components/AdvancedSection.client";
1112
import { QuickStartSection } from "./components/QuickstartSection.client";
@@ -29,6 +30,8 @@ export default async function Page(props: {
2930
redirect(`/team/${params.team_slug}`);
3031
}
3132

33+
const projectWallet = await getProjectWallet(project);
34+
3235
const client = getClientThirdwebClient({
3336
jwt: authToken,
3437
teamId: project.teamId,
@@ -51,6 +54,7 @@ export default async function Page(props: {
5154
component: (
5255
<CreatePaymentLinkButton
5356
clientId={project.publishableKey}
57+
projectWalletAddress={projectWallet?.address}
5458
teamId={project.teamId}
5559
>
5660
<Button className="gap-1.5 rounded-full" size="sm">
@@ -141,12 +145,14 @@ export default async function Page(props: {
141145
<div className="flex flex-col gap-12">
142146
<PaymentLinksTable
143147
clientId={project.publishableKey}
148+
projectWalletAddress={projectWallet?.address}
144149
teamId={project.teamId}
145150
/>
146151
<QuickStartSection
147152
projectSlug={params.project_slug}
148153
teamSlug={params.team_slug}
149154
clientId={project.publishableKey}
155+
projectWalletAddress={projectWallet?.address}
150156
teamId={project.teamId}
151157
/>
152158

0 commit comments

Comments
 (0)