diff --git a/apps/dashboard/src/@/api/linked-wallets.ts b/apps/dashboard/src/@/api/linked-wallets.ts
new file mode 100644
index 00000000000..8fe3dbf4318
--- /dev/null
+++ b/apps/dashboard/src/@/api/linked-wallets.ts
@@ -0,0 +1,32 @@
+import { getAuthToken } from "../../app/api/lib/getAuthToken";
+import { API_SERVER_URL } from "../constants/env";
+
+export type LinkedWallet = {
+ createdAt: string;
+ id: string;
+ walletAddress: string;
+};
+
+export async function getLinkedWallets() {
+ const token = await getAuthToken();
+
+ if (!token) {
+ return null;
+ }
+
+ const res = await fetch(`${API_SERVER_URL}/v1/account/wallets`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (res.ok) {
+ const json = (await res.json()) as {
+ data: LinkedWallet[];
+ };
+
+ return json.data;
+ }
+
+ return null;
+}
diff --git a/apps/dashboard/src/app/account/devices/AccountDevicesPage.tsx b/apps/dashboard/src/app/account/devices/AccountDevicesPage.tsx
index 287cbd6a9ee..a6968bedb98 100644
--- a/apps/dashboard/src/app/account/devices/AccountDevicesPage.tsx
+++ b/apps/dashboard/src/app/account/devices/AccountDevicesPage.tsx
@@ -11,25 +11,12 @@ export function AccountDevicesPage() {
const authorizedWalletsQuery = useAuthorizedWallets();
return (
-
-
-
-
-
Devices
-
- List of authorized devices that can perform actions on behalf of
- your account.
-
-
-
-
-
-
-
-
+
+
+
);
}
diff --git a/apps/dashboard/src/app/account/devices/page.tsx b/apps/dashboard/src/app/account/devices/page.tsx
index 2c51881961d..69758ec2e9a 100644
--- a/apps/dashboard/src/app/account/devices/page.tsx
+++ b/apps/dashboard/src/app/account/devices/page.tsx
@@ -5,5 +5,21 @@ export default async function Page() {
// enforce valid account
await getValidAccount("/account/devices");
- return ;
+ return (
+
+ );
}
diff --git a/apps/dashboard/src/app/account/layout.tsx b/apps/dashboard/src/app/account/layout.tsx
index 99fa94126fd..8e1764ba4d8 100644
--- a/apps/dashboard/src/app/account/layout.tsx
+++ b/apps/dashboard/src/app/account/layout.tsx
@@ -70,11 +70,10 @@ async function HeaderAndNav(props: {
path: "/account/settings",
name: "Settings",
},
- // TODO - enable these links after they are functional
- // {
- // path: "/account/wallets",
- // name: "Wallets",
- // },
+ {
+ path: "/account/wallets",
+ name: "Linked Wallets",
+ },
{
path: "/account/devices",
name: "Devices",
diff --git a/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx b/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
index acd60f27c19..e64bbcdee63 100644
--- a/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
+++ b/apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx
@@ -46,9 +46,9 @@ export function AccountTeamsUI(props: {
-
+
- Create a Team
+ Create Team
diff --git a/apps/dashboard/src/app/account/page.tsx b/apps/dashboard/src/app/account/page.tsx
index 5c77008ce29..8c7543c0c5b 100644
--- a/apps/dashboard/src/app/account/page.tsx
+++ b/apps/dashboard/src/app/account/page.tsx
@@ -1,14 +1,19 @@
import { getTeams } from "@/api/team";
import { getMembers } from "@/api/team-members";
import { getThirdwebClient } from "@/constants/thirdweb.server";
+import { getAuthToken } from "../api/lib/getAuthToken";
import { loginRedirect } from "../login/loginRedirect";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
import { getValidAccount } from "./settings/getAccount";
export default async function Page() {
- const account = await getValidAccount("/account");
- const teams = await getTeams();
- if (!teams) {
+ const [authToken, account, teams] = await Promise.all([
+ getAuthToken(),
+ getValidAccount("/account"),
+ getTeams(),
+ ]);
+
+ if (!authToken || !teams) {
loginRedirect("/account");
}
@@ -36,11 +41,19 @@ export default async function Page() {
).filter((x) => !!x);
return (
-
-
+
);
}
diff --git a/apps/dashboard/src/app/account/settings/AccountSettingsPage.tsx b/apps/dashboard/src/app/account/settings/AccountSettingsPage.tsx
index 0e903819d54..fe8c93d7514 100644
--- a/apps/dashboard/src/app/account/settings/AccountSettingsPage.tsx
+++ b/apps/dashboard/src/app/account/settings/AccountSettingsPage.tsx
@@ -15,13 +15,14 @@ export function AccountSettingsPage(props: {
const router = useDashboardRouter();
return (
-
+
+
{
+ const res = await apiServerProxy({
+ pathname: `/v1/account/wallets/${walletId}`,
+ method: "DELETE",
+ });
+
+ if (!res.ok) {
+ console.error(res.error);
+ throw new Error(res.error);
+ }
+
+ router.refresh();
+ }}
+ />
+ );
+}
+
export function LinkWalletUI(props: {
- wallets: string[];
+ wallets: LinkedWallet[];
+ unlinkWallet: (walletId: string) => Promise;
+ accountEmail: string | undefined;
}) {
+ const [deletedWalletIds, setDeletedWalletIds] = useState([]);
const [searchValue, setSearchValue] = useState("");
- const walletsToShow = !searchValue
+ let walletsToShow = !searchValue
? props.wallets
: props.wallets.filter((v) => {
- return v.toLowerCase().includes(searchValue.toLowerCase());
+ return v.walletAddress
+ .toLowerCase()
+ .includes(searchValue.toLowerCase());
});
+ walletsToShow = walletsToShow.filter((v) => !deletedWalletIds.includes(v.id));
+
return (
-
-
-
- Linked Wallets
-
-
- The wallets that are linked to your thirdweb account
-
-
-
- {/* TODO - handle linking */}
-
-
- Link a Wallet
-
-
-
-
-
-
- {walletsToShow.map((v) => {
- return (
-
-
-
- );
- })}
+
+
+
+
+ Wallet Address
+ Linked on
+ Unlink
+
+
+
+ {walletsToShow.map((wallet) => (
+
+
+
+
+
+ {formatDate(wallet.createdAt, "MMM d, yyyy")}
+
+
+ {
+ setDeletedWalletIds([...deletedWalletIds, wallet.id]);
+ }}
+ />
+
+
+ ))}
- {/* No Result Found */}
- {walletsToShow.length === 0 && (
-
-
-
No Wallets Found
- {searchValue && (
-
- Your search for {`"${searchValue}"`} did not match any wallets
-
- )}
-
-
- )}
-
+ {walletsToShow.length === 0 && (
+
+
+
+
No Wallets Found
+ {searchValue && (
+
+ Your search for {`"${searchValue}"`} did not match any
+ wallets
+
+ )}
+
+
+
+ )}
+
+
+
);
}
-function WalletRow(props: {
- address: string;
+function UnlinkButton(props: {
+ wallet: LinkedWallet;
+ unlinkWallet: (walletId: string) => Promise;
+ onUnlinkSuccess: () => void;
+ accountEmail: string | undefined;
}) {
+ const [open, setOpen] = useState(false);
+ const unlinkWallet = useMutation({
+ mutationFn: props.unlinkWallet,
+ onSuccess: () => {
+ props.onUnlinkSuccess();
+ setOpen(false);
+ toast.success("Wallet unlinked successfully");
+ },
+ onError: () => {
+ toast.error("Failed to unlink wallet");
+ },
+ });
+
return (
-
- {/* start */}
-
+
+
+
+
+ Unlink
+
+
+
+
+
+ Unlink Wallet
+
+
+ Are you sure you want to unlink this wallet?
+
+
+
- {/* end */}
- {/* TODO - handle unlink */}
-
-
- Unlink
-
-
+ {/* biome-ignore lint/a11y/noNoninteractiveTabindex: prevents autofocus inside this div to prevent hover card from opening */}
+
+
+
+
+
+
+
+ This wallet can be linked again by logging in dashboard with this
+ wallet and providing the confirmation code sent to{" "}
+ {props.accountEmail || "email address associated with this account"}
+
+
+
+
+ setOpen(false)}
+ disabled={unlinkWallet.isPending}
+ >
+ Cancel
+
+ {
+ unlinkWallet.mutate(props.wallet.id);
+ }}
+ disabled={unlinkWallet.isPending}
+ >
+ {unlinkWallet.isPending ? (
+
+ ) : (
+
+ )}
+ Unlink Wallet
+
+
+
+
);
}
diff --git a/apps/dashboard/src/app/account/wallets/LinkWalletsUI.stories.tsx b/apps/dashboard/src/app/account/wallets/LinkWalletsUI.stories.tsx
index e4da67ce1a2..519abc37c1a 100644
--- a/apps/dashboard/src/app/account/wallets/LinkWalletsUI.stories.tsx
+++ b/apps/dashboard/src/app/account/wallets/LinkWalletsUI.stories.tsx
@@ -1,4 +1,6 @@
+import type { LinkedWallet } from "@/api/linked-wallets";
import type { Meta, StoryObj } from "@storybook/react";
+import { Toaster } from "sonner";
import { ThirdwebProvider } from "thirdweb/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { LinkWalletUI } from "./LinkWalletUI";
@@ -31,31 +33,66 @@ export const Mobile: Story = {
},
};
+const unlinkWalletSuccessStub = async (walletId: string) => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("unlinkWallet", walletId);
+};
+
+const unlinkWalletFailureStub = async (walletId: string) => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("unlinkWallet", walletId);
+ throw new Error("Failed to unlink wallet");
+};
+
+const accountWalletsStub: LinkedWallet[] = [
+ {
+ walletAddress: "0x51696930092b42243dee1077c8dd237074fb28d4", // jns.eth
+ createdAt: new Date().toISOString(),
+ id: "1",
+ },
+ {
+ walletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", // no ens
+ createdAt: new Date(
+ Date.now() - 1000 * 60 * 60 * 24 * 365 * 2,
+ ).toISOString(),
+ id: "2",
+ },
+ {
+ walletAddress: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", // vitalik.eth
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
+ id: "3",
+ },
+];
+
function Variants() {
return (
-
+
-
+
-
+
+
+
);
diff --git a/apps/dashboard/src/app/account/wallets/page.tsx b/apps/dashboard/src/app/account/wallets/page.tsx
index e7d2e7e88c4..4d9eb082e59 100644
--- a/apps/dashboard/src/app/account/wallets/page.tsx
+++ b/apps/dashboard/src/app/account/wallets/page.tsx
@@ -1,12 +1,29 @@
-import { LinkWalletUI } from "./LinkWalletUI";
+import { getLinkedWallets } from "@/api/linked-wallets";
+import { getValidAccount } from "../settings/getAccount";
+import { LinkWallet } from "./LinkWalletUI";
export default async function Page() {
+ const [wallets, account] = await Promise.all([
+ getLinkedWallets(),
+ getValidAccount(),
+ ]);
+
return (
-
-
+
+
+
+
+ Linked Wallets
+
+
+
+
+
+
+
);
}