Skip to content

Commit f81c1af

Browse files
committed
[Dashboard]: enable download of all user data at once (#5320)
https://linear.app/thirdweb/issue/DASH-398/enish-cant-download-wallet-addresses-from-dashboard <!-- start pr-codex --> --- ## PR-Codex overview This PR enhances the `useEmbeddedWallets` hook by adding a new function, `fetchAccountList`, and introduces a new hook, `useAllEmbeddedWallets`, for fetching wallet data. It also improves the CSV download functionality in the `InAppWalletUsersPageContent` component. ### Detailed summary - Added `fetchAccountList` function to handle API calls for fetching wallet accounts. - Introduced `useAllEmbeddedWallets` hook for batch fetching wallet data across multiple pages. - Updated `useEmbeddedWallets` to use `fetchAccountList`. - Enhanced CSV download logic to handle cases when no wallets are available. - Integrated loading spinner in the download button to indicate pending actions. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 022e403 commit f81c1af

File tree

2 files changed

+98
-40
lines changed

2 files changed

+98
-40
lines changed
Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,43 @@
1-
import { keepPreviousData, useQuery } from "@tanstack/react-query";
1+
import {
2+
keepPreviousData,
3+
useMutation,
4+
useQuery,
5+
useQueryClient,
6+
} from "@tanstack/react-query";
27
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
38
import type { WalletUser } from "thirdweb/wallets";
49
import { embeddedWalletsKeys } from "../cache-keys";
510
import { useLoggedInUser } from "./useLoggedInUser";
611

12+
const fetchAccountList = ({
13+
jwt,
14+
clientId,
15+
pageNumber,
16+
}: { jwt: string; clientId: string; pageNumber: number }) => {
17+
return async () => {
18+
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
19+
url.searchParams.append("clientId", clientId);
20+
url.searchParams.append("page", pageNumber.toString());
21+
22+
const res = await fetch(url.href, {
23+
method: "GET",
24+
headers: {
25+
"Content-Type": "application/json",
26+
Authorization: `Bearer ${jwt}`,
27+
},
28+
});
29+
if (!res.ok) {
30+
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
31+
}
32+
33+
const json = await res.json();
34+
return json as {
35+
users: WalletUser[];
36+
totalPages: number;
37+
};
38+
};
39+
};
40+
741
export function useEmbeddedWallets(clientId: string, page: number) {
842
const { user, isLoggedIn } = useLoggedInUser();
943

@@ -13,31 +47,44 @@ export function useEmbeddedWallets(clientId: string, page: number) {
1347
clientId,
1448
page,
1549
),
16-
queryFn: async () => {
17-
const url = new URL(
18-
`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`,
19-
);
20-
url.searchParams.append("clientId", clientId);
21-
url.searchParams.append("page", page.toString());
50+
queryFn: fetchAccountList({
51+
jwt: user?.jwt ?? "",
52+
clientId,
53+
pageNumber: page,
54+
}),
55+
placeholderData: keepPreviousData,
56+
enabled: !!user?.address && isLoggedIn && !!clientId,
57+
});
58+
}
2259

23-
const res = await fetch(url.href, {
24-
method: "GET",
25-
headers: {
26-
"Content-Type": "application/json",
27-
Authorization: `Bearer ${user?.jwt}`,
28-
},
29-
});
30-
if (!res.ok) {
31-
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
60+
export function useAllEmbeddedWallets() {
61+
const queryClient = useQueryClient();
62+
const { user } = useLoggedInUser();
63+
return useMutation({
64+
mutationFn: async ({
65+
clientId,
66+
totalPages,
67+
}: { clientId: string; totalPages: number }) => {
68+
const walletRes = [];
69+
for (let page = 1; page <= totalPages; page++) {
70+
const res = queryClient.fetchQuery<{
71+
users: WalletUser[];
72+
totalPages: number;
73+
}>({
74+
queryKey: embeddedWalletsKeys.embeddedWallets(
75+
user?.address as string,
76+
clientId,
77+
page,
78+
),
79+
queryFn: fetchAccountList({
80+
jwt: user?.jwt ?? "",
81+
clientId,
82+
pageNumber: page,
83+
}),
84+
});
85+
walletRes.push(res);
3286
}
33-
34-
const json = await res.json();
35-
return json as {
36-
users: WalletUser[];
37-
totalPages: number;
38-
};
87+
return (await Promise.all(walletRes)).flatMap((res) => res.users);
3988
},
40-
placeholderData: keepPreviousData,
41-
enabled: !!user?.address && isLoggedIn && !!clientId,
4289
});
4390
}

apps/dashboard/src/components/embedded-wallets/Users/index.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import { WalletAddress } from "@/components/blocks/wallet-address";
44
import { PaginationButtons } from "@/components/pagination-buttons";
5+
import { Spinner } from "@/components/ui/Spinner/Spinner";
56
import { Button } from "@/components/ui/button";
67
import {
78
Tooltip,
89
TooltipContent,
910
TooltipProvider,
1011
TooltipTrigger,
1112
} from "@/components/ui/tooltip";
12-
import { useEmbeddedWallets } from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
13-
import { Spinner } from "@chakra-ui/react";
13+
import {
14+
useAllEmbeddedWallets,
15+
useEmbeddedWallets,
16+
} from "@3rdweb-sdk/react/hooks/useEmbeddedWallets";
1417
import { createColumnHelper } from "@tanstack/react-table";
1518
import { TWTable } from "components/shared/TWTable";
1619
import { format } from "date-fns/format";
@@ -104,22 +107,28 @@ export const InAppWalletUsersPageContent = (props: {
104107
users: [],
105108
totalPages: 1,
106109
};
110+
const { mutateAsync: getAllEmbeddedWallets, isPending } =
111+
useAllEmbeddedWallets();
107112

108-
// TODO: Make the download CSV grab all data instead of merely what's on the current page
109-
const downloadCSV = useCallback(() => {
110-
if (wallets.length === 0) {
113+
const downloadCSV = useCallback(async () => {
114+
if (wallets.length === 0 || !getAllEmbeddedWallets) {
111115
return;
112116
}
117+
const usersWallets = await getAllEmbeddedWallets({
118+
clientId: props.clientId,
119+
totalPages,
120+
});
113121
const csv = Papa.unparse(
114-
wallets.map((row) => ({
115-
user_identifier: getUserIdentifier(row.linkedAccounts),
116-
address: row.wallets[0]?.address || "",
117-
created: format(
118-
new Date(row.wallets[0]?.createdAt ?? ""),
119-
"MMM dd, yyyy",
120-
),
121-
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
122-
})),
122+
usersWallets.map((row) => {
123+
return {
124+
user_identifier: getUserIdentifier(row.linkedAccounts),
125+
address: row.wallets[0]?.address || "Uninitialized",
126+
created: row.wallets[0]?.createdAt
127+
? format(new Date(row.wallets[0].createdAt), "MMM dd, yyyy")
128+
: "Wallet not created yet",
129+
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
130+
};
131+
}),
123132
);
124133
const csvUrl = URL.createObjectURL(
125134
new Blob([csv], { type: "text/csv;charset=utf-8;" }),
@@ -128,19 +137,21 @@ export const InAppWalletUsersPageContent = (props: {
128137
tempLink.href = csvUrl;
129138
tempLink.setAttribute("download", "download.csv");
130139
tempLink.click();
131-
}, [wallets]);
140+
}, [wallets, props.clientId, totalPages, getAllEmbeddedWallets]);
132141

133142
return (
134143
<div>
135144
<div className="flex flex-col gap-6">
136145
{/* Top section */}
137146
<div className="flex items-center justify-between">
138147
<Button
139-
disabled={wallets.length === 0}
148+
disabled={wallets.length === 0 || isPending}
140149
variant="outline"
141150
onClick={downloadCSV}
142151
size="sm"
152+
className="gap-2"
143153
>
154+
{isPending && <Spinner className="size-4" />}
144155
Download as .csv
145156
</Button>
146157

0 commit comments

Comments
 (0)