diff --git a/.changeset/fuzzy-olives-prove.md b/.changeset/fuzzy-olives-prove.md new file mode 100644 index 00000000000..c452d55ad44 --- /dev/null +++ b/.changeset/fuzzy-olives-prove.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +expose WalletUser type for in app / ecosystem wallets diff --git a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts index 39062fdfdc9..1d2c0ca1f90 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts @@ -63,8 +63,11 @@ export const embeddedWalletsKeys = { all: ["embeddedWallets"] as const, wallet: (walletAddress: string) => [...embeddedWalletsKeys.all, walletAddress] as const, - embeddedWallets: (walletAddress: string, clientId: string | undefined) => - [...embeddedWalletsKeys.wallet(walletAddress), clientId] as const, + embeddedWallets: ( + walletAddress: string, + clientId: string | undefined, + page: number, + ) => [...embeddedWalletsKeys.wallet(walletAddress), clientId, page] as const, }; export const engineKeys = { diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts index 7fe6733efb9..e0b2e57caed 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEmbeddedWallets.ts @@ -1,51 +1,43 @@ -import { useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { THIRDWEB_EWS_API_HOST } from "constants/urls"; +import type { WalletUser } from "thirdweb/wallets"; import { embeddedWalletsKeys } from "../cache-keys"; import { useLoggedInUser } from "./useLoggedInUser"; -// FIXME: Make API to return camelCase or transform -export type EmbeddedWalletUser = { - id: string; - client_id: string; - created_at: string; - last_accessed_at: string; - embedded_wallet?: { - id: string; - address: string; - chain: string; - wallet_user_id: string; - }[]; - ews_authed_user: { - id: string; - authed_user_id: string; - email: string; - }[]; -}; - -export function useEmbeddedWallets(clientId: string) { +export function useEmbeddedWallets(clientId: string, page: number) { const { user, isLoggedIn } = useLoggedInUser(); return useQuery({ queryKey: embeddedWalletsKeys.embeddedWallets( user?.address as string, - clientId as string, + clientId, + page, ), queryFn: async () => { - const res = await fetch( - `${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${user?.jwt}`, - }, - }, + const url = new URL( + `${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`, ); + url.searchParams.append("clientId", clientId); + url.searchParams.append("page", page.toString()); - const json = await res.json(); + const res = await fetch(url.href, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${user?.jwt}`, + }, + }); + if (!res.ok) { + throw new Error(`Failed to fetch wallets: ${await res.text()}`); + } - return json.walletUsers as EmbeddedWalletUser[]; + const json = await res.json(); + return json as { + users: WalletUser[]; + totalPages: number; + }; }, + placeholderData: keepPreviousData, enabled: !!user?.address && isLoggedIn && !!clientId, }); } diff --git a/apps/dashboard/src/components/embedded-wallets/Users/index.tsx b/apps/dashboard/src/components/embedded-wallets/Users/index.tsx index 06eeff99fe0..130ea4bd9b1 100644 --- a/apps/dashboard/src/components/embedded-wallets/Users/index.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Users/index.tsx @@ -3,41 +3,54 @@ import { WalletAddress } from "@/components/blocks/wallet-address"; import { PaginationButtons } from "@/components/pagination-buttons"; import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; import { - type EmbeddedWalletUser, - useEmbeddedWallets, -} from "@3rdweb-sdk/react/hooks/useEmbeddedWallets"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets"; +import { Spinner } from "@chakra-ui/react"; import { createColumnHelper } from "@tanstack/react-table"; import { TWTable } from "components/shared/TWTable"; import { format } from "date-fns/format"; import Papa from "papaparse"; -import { useCallback, useMemo, useState } from "react"; -import { withinDays } from "utils/date-utils"; +import { useCallback, useState } from "react"; +import type { WalletUser } from "thirdweb/wallets"; -const ACTIVE_THRESHOLD_DAYS = 30; - -const columnHelper = createColumnHelper(); +const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => { + const mainDetail = accounts[0]?.details; + return ( + mainDetail?.email ?? + mainDetail?.phone ?? + mainDetail?.address ?? + mainDetail?.id + ); +}; +const columnHelper = createColumnHelper(); const columns = [ - columnHelper.accessor("ews_authed_user", { - header: "Email", + columnHelper.accessor("linkedAccounts", { + header: "User Identifier", enableColumnFilter: true, - cell: (cell) => ( - {cell.getValue()?.[0]?.email} - ), + cell: (cell) => { + const identifier = getUserIdentifier(cell.getValue()); + return {identifier}; + }, + id: "user_identifier", }), - columnHelper.accessor("embedded_wallet", { + columnHelper.accessor("wallets", { header: "Address", cell: (cell) => { - const address = cell.getValue()?.[0]?.address; + const address = cell.getValue()[0]?.address; return address ? : null; }, + id: "address", }), - columnHelper.accessor("created_at", { + columnHelper.accessor("wallets", { header: "Created", cell: (cell) => { - const value = cell.getValue(); + const value = cell.getValue()[0]?.createdAt; if (!value) { return; @@ -48,21 +61,36 @@ const columns = [ ); }, + id: "created_at", }), - columnHelper.accessor("last_accessed_at", { - header: "Last login", + columnHelper.accessor("linkedAccounts", { + header: "Login Methods", cell: (cell) => { const value = cell.getValue(); - - if (!value) { - return; - } + const loginMethodsDisplay = value.reduce((acc, curr) => { + if (acc.length === 2) { + acc.push("..."); + } + if (acc.length < 2) { + acc.push(curr.type); + } + return acc; + }, [] as string[]); + const loginMethods = value.map((v) => v.type).join(", "); return ( - - {format(new Date(value), "MMM dd, yyyy")} - + + + + {loginMethodsDisplay.join(", ")} + + + {loginMethods} + + + ); }, + id: "login_methods", }), ]; @@ -70,37 +98,27 @@ export const InAppWalletUsersPageContent = (props: { clientId: string; trackingCategory: string; }) => { - const [onlyActive, setOnlyActive] = useState(true); - const walletsQuery = useEmbeddedWallets(props.clientId); - const wallets = walletsQuery?.data || []; - - const activeWallets = useMemo(() => { - if (!wallets) { - return []; - } - - return wallets.filter((w) => { - const lastAccessedAt = w.last_accessed_at; - return ( - lastAccessedAt && withinDays(lastAccessedAt, ACTIVE_THRESHOLD_DAYS) - ); - }); - }, [wallets]); - - const theWalletsWeWant = useMemo(() => { - return (onlyActive ? activeWallets : wallets) ?? []; - }, [activeWallets, onlyActive, wallets]); + const [activePage, setActivePage] = useState(1); + const walletsQuery = useEmbeddedWallets(props.clientId, activePage); + const { users: wallets, totalPages } = walletsQuery?.data || { + users: [], + totalPages: 1, + }; + // TODO: Make the download CSV grab all data instead of merely what's on the current page const downloadCSV = useCallback(() => { - if (theWalletsWeWant.length === 0) { + if (wallets.length === 0) { return; } const csv = Papa.unparse( - theWalletsWeWant.map((row) => ({ - email: row.ews_authed_user[0]?.email, - address: row.embedded_wallet?.[0]?.address || "", - created: format(new Date(row.created_at), "MMM dd, yyyy"), - last_login: format(new Date(row.last_accessed_at), "MMM dd, yyyy"), + wallets.map((row) => ({ + user_identifier: getUserIdentifier(row.linkedAccounts), + address: row.wallets[0]?.address || "", + created: format( + new Date(row.wallets[0]?.createdAt ?? ""), + "MMM dd, yyyy", + ), + login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "), })), ); const csvUrl = URL.createObjectURL( @@ -110,20 +128,7 @@ export const InAppWalletUsersPageContent = (props: { tempLink.href = csvUrl; tempLink.setAttribute("download", "download.csv"); tempLink.click(); - }, [theWalletsWeWant]); - - const [activePage, setActivePage] = useState(1); - const itemsPerPage = 20; - const totalPages = - theWalletsWeWant.length <= itemsPerPage - ? 1 - : Math.ceil(theWalletsWeWant.length / itemsPerPage); - - const itemsToShow = useMemo(() => { - const startIndex = (activePage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - return theWalletsWeWant.slice(startIndex, endIndex); - }, [activePage, theWalletsWeWant]); + }, [wallets]); return (
@@ -131,7 +136,7 @@ export const InAppWalletUsersPageContent = (props: { {/* Top section */}