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 ( +
+
+
+

Devices

+

+ List of authorized devices that can perform actions on behalf of + your account. +

+
+
+ +
+ +
+
+ ); } 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: { - 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 ( -
- +
+
+
+

Overview

+
+
+ +
+ +
); } 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 (
-
+

Account Settings

-
+ +
{ + 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 */} - -
- -
- -
    - {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 Wallet + + + Are you sure you want to unlink this wallet? + + + - {/* end */} - {/* TODO - handle 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"} +

+
+ +
+ + +
+ + ); } 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 +

+
+
+ +
+ +
); }