Skip to content

Commit ae75073

Browse files
authored
Refactor project wallet setup and controls UI (#8220)
1 parent 781a626 commit ae75073

File tree

4 files changed

+289
-64
lines changed

4 files changed

+289
-64
lines changed

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

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import Link from "next/link";
88
import type { Project } from "@/api/project/projects";
99
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
10-
import { AlertDialogFooter } from "@/components/ui/alert-dialog";
1110
import { CodeServer } from "@/components/ui/code/code.server";
1211
import { UnderlineLink } from "@/components/ui/UnderlineLink";
1312
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
@@ -25,10 +24,10 @@ import {
2524
getProjectWallet,
2625
type ProjectWalletSummary,
2726
} from "@/lib/server/project-wallet";
28-
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
2927
import { ClientIDSection } from "./ClientIDSection";
3028
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
3129
import { ProjectWalletControls } from "./ProjectWalletControls.client";
30+
import { ProjectWalletSetup } from "./ProjectWalletSetup.client";
3231
import { SecretKeySection } from "./SecretKeySection";
3332

3433
export async function ProjectFTUX(props: {
@@ -108,22 +107,11 @@ export function ProjectWalletSection(props: {
108107
</div>
109108
</>
110109
) : (
111-
<Alert variant="info">
112-
<CircleAlertIcon className="size-5" />
113-
<AlertTitle>No default project wallet set</AlertTitle>
114-
<AlertDescription>
115-
Set a default project wallet to use for dashboard and API
116-
integrations.
117-
</AlertDescription>
118-
<AlertDialogFooter className="flex justify-start sm:justify-start pt-4">
119-
<CreateServerWallet
120-
managementAccessToken={props.managementAccessToken}
121-
project={props.project}
122-
teamSlug={props.teamSlug}
123-
setAsProjectWallet={true}
124-
/>
125-
</AlertDialogFooter>
126-
</Alert>
110+
<ProjectWalletSetup
111+
managementAccessToken={props.managementAccessToken}
112+
project={props.project}
113+
teamSlug={props.teamSlug}
114+
/>
127115
)}
128116
</div>
129117
</div>

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

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,14 @@ import {
4848
FormMessage,
4949
} from "@/components/ui/form";
5050
import { Input } from "@/components/ui/input";
51-
import { Label } from "@/components/ui/label";
52-
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
5351
import { Spinner } from "@/components/ui/Spinner";
52+
import {
53+
Select,
54+
SelectContent,
55+
SelectItem,
56+
SelectTrigger,
57+
SelectValue,
58+
} from "@/components/ui/select";
5459
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
5560
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
5661
import { useDashboardRouter } from "@/lib/DashboardRouter";
@@ -163,7 +168,7 @@ export function ProjectWalletControls(props: ProjectWalletControlsProps) {
163168
},
164169
onSuccess: async (_, wallet) => {
165170
const descriptionLabel = wallet.label ?? wallet.address;
166-
toast.success("Default project wallet updated", {
171+
toast.success("Project wallet updated", {
167172
description: `Now pointing to ${descriptionLabel}`,
168173
});
169174
await queryClient.invalidateQueries({
@@ -347,49 +352,66 @@ export function ProjectWalletControls(props: ProjectWalletControlsProps) {
347352
default.
348353
</p>
349354
) : (
350-
<RadioGroup
351-
className="space-y-3"
352-
onValueChange={(value) => setSelectedWalletId(value)}
353-
value={selectedWalletId}
354-
>
355-
{serverWallets.map((wallet) => {
356-
const isCurrent =
357-
wallet.address.toLowerCase() === currentWalletAddressLower;
358-
const isSelected = wallet.id === selectedWalletId;
359-
360-
return (
361-
<Label
362-
key={wallet.id}
363-
htmlFor={`project-wallet-${wallet.id}`}
364-
className={cn(
365-
"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-card px-4 py-3 transition",
366-
isSelected && "border-primary ring-2 ring-primary/20",
367-
)}
368-
>
369-
<RadioGroupItem
370-
id={`project-wallet-${wallet.id}`}
371-
value={wallet.id}
372-
className="mt-0.5"
373-
/>
374-
<div className="flex flex-1 flex-col overflow-hidden">
355+
<div className="flex flex-col gap-3">
356+
<Select
357+
onValueChange={(value) => setSelectedWalletId(value)}
358+
value={selectedWalletId ?? ""}
359+
>
360+
<SelectTrigger className="w-full">
361+
<SelectValue placeholder="Select server wallet">
362+
{selectedWallet ? (
375363
<div className="flex items-center gap-2">
376-
<span className="text-sm font-medium text-foreground">
377-
{wallet.label ?? "Unnamed server wallet"}
378-
</span>
379-
{isCurrent && (
380-
<Badge variant="success" className="text-xs">
381-
current
382-
</Badge>
383-
)}
364+
<WalletAddress
365+
address={selectedWallet.address}
366+
client={client}
367+
/>
368+
<div className="flex items-center gap-2">
369+
<span className="text-sm text-foreground">
370+
{selectedWallet.label ?? "Unnamed server wallet"}
371+
</span>
372+
{selectedWallet.address.toLowerCase() ===
373+
currentWalletAddressLower && (
374+
<Badge variant="success" className="text-xs">
375+
current
376+
</Badge>
377+
)}
378+
</div>
384379
</div>
385-
<span className="text-xs text-muted-foreground break-all">
386-
{wallet.address}
387-
</span>
388-
</div>
389-
</Label>
390-
);
391-
})}
392-
</RadioGroup>
380+
) : (
381+
"Select server wallet"
382+
)}
383+
</SelectValue>
384+
</SelectTrigger>
385+
<SelectContent>
386+
{serverWallets.map((wallet) => {
387+
const isCurrent =
388+
wallet.address.toLowerCase() ===
389+
currentWalletAddressLower;
390+
391+
return (
392+
<SelectItem key={wallet.id} value={wallet.id}>
393+
<div className="flex items-center gap-2">
394+
<WalletAddress
395+
address={wallet.address}
396+
client={client}
397+
/>
398+
<div className="flex items-center gap-2">
399+
<span className="text-sm text-foreground">
400+
{wallet.label ?? "Unnamed server wallet"}
401+
</span>
402+
{isCurrent && (
403+
<Badge variant="success" className="text-xs">
404+
current
405+
</Badge>
406+
)}
407+
</div>
408+
</div>
409+
</SelectItem>
410+
);
411+
})}
412+
</SelectContent>
413+
</Select>
414+
</div>
393415
)}
394416
</div>
395417

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"use client";
2+
3+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4+
import { CircleAlertIcon } from "lucide-react";
5+
import { useMemo, useState } from "react";
6+
import { toast } from "sonner";
7+
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
8+
import type { Project } from "@/api/project/projects";
9+
import { WalletAddress } from "@/components/blocks/wallet-address";
10+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
11+
import { Button } from "@/components/ui/button";
12+
import { Spinner } from "@/components/ui/Spinner";
13+
import {
14+
Select,
15+
SelectContent,
16+
SelectItem,
17+
SelectTrigger,
18+
SelectValue,
19+
} from "@/components/ui/select";
20+
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
21+
import { useDashboardRouter } from "@/lib/DashboardRouter";
22+
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
23+
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
24+
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
25+
26+
export function ProjectWalletSetup(props: {
27+
project: Project;
28+
teamSlug: string;
29+
managementAccessToken: string | undefined;
30+
}) {
31+
const { project, teamSlug, managementAccessToken } = props;
32+
const router = useDashboardRouter();
33+
const queryClient = useQueryClient();
34+
const client = useMemo(() => getClientThirdwebClient(), []);
35+
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
36+
undefined,
37+
);
38+
39+
const serverWalletsQuery = useQuery({
40+
enabled: Boolean(managementAccessToken),
41+
queryFn: async () => {
42+
if (!managementAccessToken) {
43+
return [] as ProjectWalletSummary[];
44+
}
45+
46+
return listProjectServerWallets({
47+
managementAccessToken,
48+
projectId: project.id,
49+
});
50+
},
51+
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
52+
staleTime: 60_000,
53+
});
54+
55+
const serverWallets = serverWalletsQuery.data ?? [];
56+
57+
const effectiveSelectedWalletId = useMemo(() => {
58+
if (
59+
selectedWalletId &&
60+
serverWallets.some((wallet) => wallet.id === selectedWalletId)
61+
) {
62+
return selectedWalletId;
63+
}
64+
65+
return serverWallets[0]?.id;
66+
}, [selectedWalletId, serverWallets]);
67+
68+
const selectedWallet = useMemo(() => {
69+
if (!effectiveSelectedWalletId) {
70+
return undefined;
71+
}
72+
73+
return serverWallets.find(
74+
(wallet) => wallet.id === effectiveSelectedWalletId,
75+
);
76+
}, [effectiveSelectedWalletId, serverWallets]);
77+
78+
const setDefaultWalletMutation = useMutation({
79+
mutationFn: async (wallet: ProjectWalletSummary) => {
80+
await updateDefaultProjectWallet({
81+
project,
82+
projectWalletAddress: wallet.address,
83+
});
84+
},
85+
onError: (error) => {
86+
const message =
87+
error instanceof Error
88+
? error.message
89+
: "Failed to update project wallet";
90+
toast.error(message);
91+
},
92+
onSuccess: async (_, wallet) => {
93+
const descriptionLabel = wallet.label ?? wallet.address;
94+
toast.success("Project wallet updated", {
95+
description: `Now pointing to ${descriptionLabel}`,
96+
});
97+
await queryClient.invalidateQueries({
98+
queryKey: [
99+
"project",
100+
project.id,
101+
"server-wallets",
102+
managementAccessToken,
103+
],
104+
});
105+
router.refresh();
106+
},
107+
});
108+
109+
const canSelectExisting =
110+
Boolean(managementAccessToken) && serverWallets.length > 0;
111+
112+
return (
113+
<Alert variant="info">
114+
<CircleAlertIcon className="size-5" />
115+
<AlertTitle>No project wallet set</AlertTitle>
116+
<AlertDescription>
117+
Set a project wallet to use for dashboard and API integrations.
118+
</AlertDescription>
119+
120+
<div className="mt-4 space-y-5">
121+
{serverWalletsQuery.isLoading ? (
122+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
123+
<Spinner className="size-4" />
124+
<span>Loading existing server wallets…</span>
125+
</div>
126+
) : canSelectExisting ? (
127+
<div className="space-y-4">
128+
<div className="space-y-2">
129+
<p className="text-sm font-medium text-foreground">
130+
Choose an existing server wallet
131+
</p>
132+
<p className="text-sm text-muted-foreground">
133+
These wallets were already created for this project. Pick one to
134+
set as the default.
135+
</p>
136+
<Select
137+
onValueChange={(value) => setSelectedWalletId(value)}
138+
value={effectiveSelectedWalletId ?? ""}
139+
>
140+
<SelectTrigger className="w-full">
141+
<SelectValue placeholder="Select server wallet">
142+
{selectedWallet ? (
143+
<div className="flex items-center gap-2">
144+
<WalletAddress
145+
address={selectedWallet.address}
146+
client={client}
147+
/>
148+
<span className="text-muted-foreground text-sm">
149+
{selectedWallet.label ?? "Unnamed server wallet"}
150+
</span>
151+
</div>
152+
) : (
153+
"Select server wallet"
154+
)}
155+
</SelectValue>
156+
</SelectTrigger>
157+
<SelectContent>
158+
{serverWallets.map((wallet) => (
159+
<SelectItem key={wallet.id} value={wallet.id}>
160+
<div className="flex items-center gap-2">
161+
<WalletAddress
162+
address={wallet.address}
163+
client={client}
164+
/>
165+
<span className="text-muted-foreground text-sm">
166+
{wallet.label ?? "Unnamed server wallet"}
167+
</span>
168+
</div>
169+
</SelectItem>
170+
))}
171+
</SelectContent>
172+
</Select>
173+
</div>
174+
175+
<div className="flex flex-wrap items-center gap-3">
176+
<Button
177+
disabled={!selectedWallet || setDefaultWalletMutation.isPending}
178+
onClick={() => {
179+
if (selectedWallet) {
180+
setDefaultWalletMutation.mutate(selectedWallet);
181+
}
182+
}}
183+
type="button"
184+
>
185+
{setDefaultWalletMutation.isPending && (
186+
<Spinner className="mr-2 size-4" />
187+
)}
188+
Set as project wallet
189+
</Button>
190+
<CreateServerWallet
191+
managementAccessToken={managementAccessToken}
192+
project={project}
193+
teamSlug={teamSlug}
194+
setAsProjectWallet
195+
/>
196+
</div>
197+
</div>
198+
) : (
199+
<div className="space-y-3 text-sm text-muted-foreground">
200+
<p>
201+
Create a server wallet to start using transactions and other
202+
project features.
203+
</p>
204+
<CreateServerWallet
205+
managementAccessToken={managementAccessToken}
206+
project={project}
207+
teamSlug={teamSlug}
208+
setAsProjectWallet
209+
/>
210+
</div>
211+
)}
212+
</div>
213+
</Alert>
214+
);
215+
}

0 commit comments

Comments
 (0)