Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import Link from "next/link";
import type { Project } from "@/api/project/projects";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertDialogFooter } from "@/components/ui/alert-dialog";
import { CodeServer } from "@/components/ui/code/code.server";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
Expand All @@ -25,10 +24,10 @@ import {
getProjectWallet,
type ProjectWalletSummary,
} from "@/lib/server/project-wallet";
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";
import { ClientIDSection } from "./ClientIDSection";
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
import { ProjectWalletControls } from "./ProjectWalletControls.client";
import { ProjectWalletSetup } from "./ProjectWalletSetup.client";
import { SecretKeySection } from "./SecretKeySection";

export async function ProjectFTUX(props: {
Expand Down Expand Up @@ -108,22 +107,11 @@ export function ProjectWalletSection(props: {
</div>
</>
) : (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>No default project wallet set</AlertTitle>
<AlertDescription>
Set a default project wallet to use for dashboard and API
integrations.
</AlertDescription>
<AlertDialogFooter className="flex justify-start sm:justify-start pt-4">
<CreateServerWallet
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
setAsProjectWallet={true}
/>
</AlertDialogFooter>
</Alert>
<ProjectWalletSetup
managementAccessToken={props.managementAccessToken}
project={props.project}
teamSlug={props.teamSlug}
/>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { useDashboardRouter } from "@/lib/DashboardRouter";
Expand Down Expand Up @@ -163,7 +168,7 @@ export function ProjectWalletControls(props: ProjectWalletControlsProps) {
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Default project wallet updated", {
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
Expand Down Expand Up @@ -347,49 +352,66 @@ export function ProjectWalletControls(props: ProjectWalletControlsProps) {
default.
</p>
) : (
<RadioGroup
className="space-y-3"
onValueChange={(value) => setSelectedWalletId(value)}
value={selectedWalletId}
>
{serverWallets.map((wallet) => {
const isCurrent =
wallet.address.toLowerCase() === currentWalletAddressLower;
const isSelected = wallet.id === selectedWalletId;

return (
<Label
key={wallet.id}
htmlFor={`project-wallet-${wallet.id}`}
className={cn(
"flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-card px-4 py-3 transition",
isSelected && "border-primary ring-2 ring-primary/20",
)}
>
<RadioGroupItem
id={`project-wallet-${wallet.id}`}
value={wallet.id}
className="mt-0.5"
/>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-col gap-3">
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={selectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">
{wallet.label ?? "Unnamed server wallet"}
</span>
{isCurrent && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
{selectedWallet.address.toLowerCase() ===
currentWalletAddressLower && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
<span className="text-xs text-muted-foreground break-all">
{wallet.address}
</span>
</div>
</Label>
);
})}
</RadioGroup>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => {
const isCurrent =
wallet.address.toLowerCase() ===
currentWalletAddressLower;

return (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{wallet.label ?? "Unnamed server wallet"}
</span>
{isCurrent && (
<Badge variant="success" className="text-xs">
current
</Badge>
)}
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"use client";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CircleAlertIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { listProjectServerWallets } from "@/actions/project-wallet/list-server-wallets";
import type { Project } from "@/api/project/projects";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
import CreateServerWallet from "../../transactions/server-wallets/components/create-server-wallet.client";

export function ProjectWalletSetup(props: {
project: Project;
teamSlug: string;
managementAccessToken: string | undefined;
}) {
const { project, teamSlug, managementAccessToken } = props;
const router = useDashboardRouter();
const queryClient = useQueryClient();
const client = useMemo(() => getClientThirdwebClient(), []);
const [selectedWalletId, setSelectedWalletId] = useState<string | undefined>(
undefined,
);

const serverWalletsQuery = useQuery({
enabled: Boolean(managementAccessToken),
queryFn: async () => {
if (!managementAccessToken) {
return [] as ProjectWalletSummary[];
}

return listProjectServerWallets({
managementAccessToken,
projectId: project.id,
});
},
queryKey: ["project", project.id, "server-wallets", managementAccessToken],
staleTime: 60_000,
});

const serverWallets = serverWalletsQuery.data ?? [];

const effectiveSelectedWalletId = useMemo(() => {
if (
selectedWalletId &&
serverWallets.some((wallet) => wallet.id === selectedWalletId)
) {
return selectedWalletId;
}

return serverWallets[0]?.id;
}, [selectedWalletId, serverWallets]);

const selectedWallet = useMemo(() => {
if (!effectiveSelectedWalletId) {
return undefined;
}

return serverWallets.find(
(wallet) => wallet.id === effectiveSelectedWalletId,
);
}, [effectiveSelectedWalletId, serverWallets]);

const setDefaultWalletMutation = useMutation({
mutationFn: async (wallet: ProjectWalletSummary) => {
await updateDefaultProjectWallet({
project,
projectWalletAddress: wallet.address,
});
},
onError: (error) => {
const message =
error instanceof Error
? error.message
: "Failed to update project wallet";
toast.error(message);
},
onSuccess: async (_, wallet) => {
const descriptionLabel = wallet.label ?? wallet.address;
toast.success("Project wallet updated", {
description: `Now pointing to ${descriptionLabel}`,
});
await queryClient.invalidateQueries({
queryKey: [
"project",
project.id,
"server-wallets",
managementAccessToken,
],
});
router.refresh();
},
});

const canSelectExisting =
Boolean(managementAccessToken) && serverWallets.length > 0;

return (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>No project wallet set</AlertTitle>
<AlertDescription>
Set a project wallet to use for dashboard and API integrations.
</AlertDescription>

<div className="mt-4 space-y-5">
{serverWalletsQuery.isLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Spinner className="size-4" />
<span>Loading existing server wallets…</span>
</div>
) : canSelectExisting ? (
<div className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
Choose an existing server wallet
</p>
<p className="text-sm text-muted-foreground">
These wallets were already created for this project. Pick one to
set as the default.
</p>
<Select
onValueChange={(value) => setSelectedWalletId(value)}
value={effectiveSelectedWalletId ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server wallet">
{selectedWallet ? (
<div className="flex items-center gap-2">
<WalletAddress
address={selectedWallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{selectedWallet.label ?? "Unnamed server wallet"}
</span>
</div>
) : (
"Select server wallet"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{serverWallets.map((wallet) => (
<SelectItem key={wallet.id} value={wallet.id}>
<div className="flex items-center gap-2">
<WalletAddress
address={wallet.address}
client={client}
/>
<span className="text-muted-foreground text-sm">
{wallet.label ?? "Unnamed server wallet"}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="flex flex-wrap items-center gap-3">
<Button
disabled={!selectedWallet || setDefaultWalletMutation.isPending}
onClick={() => {
if (selectedWallet) {
setDefaultWalletMutation.mutate(selectedWallet);
}
}}
type="button"
>
{setDefaultWalletMutation.isPending && (
<Spinner className="mr-2 size-4" />
)}
Set as project wallet
</Button>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
</div>
) : (
<div className="space-y-3 text-sm text-muted-foreground">
<p>
Create a server wallet to start using transactions and other
project features.
</p>
<CreateServerWallet
managementAccessToken={managementAccessToken}
project={project}
teamSlug={teamSlug}
setAsProjectWallet
/>
</div>
)}
</div>
</Alert>
);
}
Loading
Loading