Skip to content

Commit 6382594

Browse files
committed
Add CSV export progress for user wallets
Track and display progress while exporting all user wallets to CSV. Introduces csvExportProgress state and a csvExportLabel to show preparation/export percentage or counts. Splits useAllEmbeddedWallets usage into two instances (one for balances, one for CSV) to avoid conflating loading states. Updates downloadCSV to call getAllEmbeddedWalletsForCsv with an onProgress callback, set progress before fetching and clear it in finally. Extends useAllEmbeddedWallets/fetchAccountList to surface pagination totalCount and invoke onProgress with fetchedWallets/totalWallets during paging.
1 parent a82e633 commit 6382594

2 files changed

Lines changed: 95 additions & 40 deletions

File tree

apps/dashboard/src/@/components/in-app-wallet-users-content/user-wallets-table.tsx

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export function UserWalletsTable(
104104
completed: 0,
105105
total: 0,
106106
});
107+
const [csvExportProgress, setCsvExportProgress] = useState<{
108+
fetchedWallets: number;
109+
totalWallets?: number;
110+
}>();
107111

108112
const walletsQuery = useEmbeddedWallets({
109113
authToken: props.authToken,
@@ -113,10 +117,18 @@ export function UserWalletsTable(
113117
page: activePage,
114118
});
115119
const wallets = walletsQuery?.data?.users || [];
116-
const { mutateAsync: getAllEmbeddedWallets, isPending: isLoadingAllWallets } =
117-
useAllEmbeddedWallets({
118-
authToken: props.authToken,
119-
});
120+
const {
121+
mutateAsync: getAllEmbeddedWalletsForBalances,
122+
isPending: isLoadingWalletsForBalances,
123+
} = useAllEmbeddedWallets({
124+
authToken: props.authToken,
125+
});
126+
const {
127+
mutateAsync: getAllEmbeddedWalletsForCsv,
128+
isPending: isLoadingWalletsForCsv,
129+
} = useAllEmbeddedWallets({
130+
authToken: props.authToken,
131+
});
120132

121133
const fetchPortfoliosMutation = useFetchAllPortfolios();
122134

@@ -125,7 +137,7 @@ export function UserWalletsTable(
125137

126138
try {
127139
// First get all wallets
128-
const allWallets = await getAllEmbeddedWallets({
140+
const allWallets = await getAllEmbeddedWalletsForBalances({
129141
clientId: props.projectClientId,
130142
ecosystemSlug: props.ecosystemSlug,
131143
teamId: props.teamId,
@@ -161,7 +173,7 @@ export function UserWalletsTable(
161173
}
162174
}, [
163175
selectedChains,
164-
getAllEmbeddedWallets,
176+
getAllEmbeddedWalletsForBalances,
165177
props.projectClientId,
166178
props.ecosystemSlug,
167179
props.teamId,
@@ -170,7 +182,32 @@ export function UserWalletsTable(
170182
]);
171183

172184
const isFetchingBalances =
173-
isLoadingAllWallets || fetchPortfoliosMutation.isPending;
185+
isLoadingWalletsForBalances || fetchPortfoliosMutation.isPending;
186+
187+
const csvExportLabel = useMemo(() => {
188+
if (!isLoadingWalletsForCsv) {
189+
return "Download as .csv";
190+
}
191+
192+
const fetchedWallets = csvExportProgress?.fetchedWallets ?? 0;
193+
const totalWallets = csvExportProgress?.totalWallets;
194+
195+
if (fetchedWallets === 0) {
196+
return "Preparing export...";
197+
}
198+
199+
if (totalWallets && totalWallets > 0) {
200+
const cappedFetchedWallets = Math.min(fetchedWallets, totalWallets);
201+
const percentage = Math.min(
202+
100,
203+
Math.round((cappedFetchedWallets / totalWallets) * 100),
204+
);
205+
206+
return `Exporting ${percentage}% (${cappedFetchedWallets.toLocaleString()}/${totalWallets.toLocaleString()})`;
207+
}
208+
209+
return `Exporting ${fetchedWallets.toLocaleString()} users...`;
210+
}, [csvExportProgress, isLoadingWalletsForCsv]);
174211

175212
const aggregatedStats = useMemo(() => {
176213
let fundedWallets = 0;
@@ -414,41 +451,48 @@ export function UserWalletsTable(
414451
};
415452

416453
const downloadCSV = useCallback(async () => {
417-
if (wallets.length === 0 || !getAllEmbeddedWallets) {
454+
if (wallets.length === 0) {
418455
return;
419456
}
420-
const usersWallets = await getAllEmbeddedWallets({
421-
clientId: props.projectClientId,
422-
ecosystemSlug: props.ecosystemSlug,
423-
teamId: props.teamId,
424-
});
425-
const csv = Papa.unparse(
426-
usersWallets.map((row) => {
427-
const email = getPrimaryEmail(row.linkedAccounts);
428-
429-
return {
430-
address: row.wallets[0]?.address || "Uninitialized",
431-
created: row.wallets[0]?.createdAt
432-
? new Date(row.wallets[0].createdAt).toISOString()
433-
: "Wallet not created yet",
434-
email: email || "N/A",
435-
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
436-
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
437-
user_identifier: row.id || "N/A",
438-
};
439-
}),
440-
);
441-
const csvUrl = URL.createObjectURL(
442-
new Blob([csv], { type: "text/csv;charset=utf-8;" }),
443-
);
444-
const tempLink = document.createElement("a");
445-
tempLink.href = csvUrl;
446-
tempLink.setAttribute("download", "download.csv");
447-
tempLink.click();
457+
setCsvExportProgress({ fetchedWallets: 0 });
458+
459+
try {
460+
const usersWallets = await getAllEmbeddedWalletsForCsv({
461+
clientId: props.projectClientId,
462+
ecosystemSlug: props.ecosystemSlug,
463+
teamId: props.teamId,
464+
onProgress: setCsvExportProgress,
465+
});
466+
const csv = Papa.unparse(
467+
usersWallets.map((row) => {
468+
const email = getPrimaryEmail(row.linkedAccounts);
469+
470+
return {
471+
address: row.wallets[0]?.address || "Uninitialized",
472+
created: row.wallets[0]?.createdAt
473+
? new Date(row.wallets[0].createdAt).toISOString()
474+
: "Wallet not created yet",
475+
email: email || "N/A",
476+
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
477+
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
478+
user_identifier: row.id || "N/A",
479+
};
480+
}),
481+
);
482+
const csvUrl = URL.createObjectURL(
483+
new Blob([csv], { type: "text/csv;charset=utf-8;" }),
484+
);
485+
const tempLink = document.createElement("a");
486+
tempLink.href = csvUrl;
487+
tempLink.setAttribute("download", "download.csv");
488+
tempLink.click();
489+
} finally {
490+
setCsvExportProgress(undefined);
491+
}
448492
}, [
449493
wallets,
450494
props.projectClientId,
451-
getAllEmbeddedWallets,
495+
getAllEmbeddedWalletsForCsv,
452496
props.teamId,
453497
props.ecosystemSlug,
454498
]);
@@ -481,12 +525,12 @@ export function UserWalletsTable(
481525
</div>
482526
<Button
483527
className="gap-2 bg-background rounded-full"
484-
disabled={wallets.length === 0 || isLoadingAllWallets}
528+
disabled={wallets.length === 0 || isLoadingWalletsForCsv}
485529
onClick={downloadCSV}
486530
variant="outline"
487531
>
488-
{isLoadingAllWallets && <Spinner className="size-4" />}
489-
Download as .csv
532+
{isLoadingWalletsForCsv && <Spinner className="size-4" />}
533+
{csvExportLabel}
490534
</Button>
491535
</div>
492536
</div>

apps/dashboard/src/@/hooks/useEmbeddedWallets.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const fetchAccountList = ({
6868
return {
6969
users: response.data.result.wallets.map(transformToWalletUser),
7070
hasMore: response.data.result.pagination.hasMore ?? false,
71+
totalCount: response.data.result.pagination.totalCount,
7172
};
7273
} catch (error) {
7374
console.error("Failed to fetch wallets:", error);
@@ -170,10 +171,15 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
170171
clientId,
171172
ecosystemSlug,
172173
teamId,
174+
onProgress,
173175
}: {
174176
clientId?: string;
175177
ecosystemSlug?: string;
176178
teamId: string;
179+
onProgress?: (progress: {
180+
fetchedWallets: number;
181+
totalWallets?: number;
182+
}) => void;
177183
}) => {
178184
const responses: WalletUser[] = [];
179185
let page = 1;
@@ -185,6 +191,7 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
185191
const res = await queryClient.fetchQuery<{
186192
users: WalletUser[];
187193
hasMore: boolean;
194+
totalCount?: number;
188195
}>({
189196
queryFn: fetchAccountList({
190197
clientId,
@@ -204,6 +211,10 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
204211
});
205212

206213
responses.push(...res.users);
214+
onProgress?.({
215+
fetchedWallets: responses.length,
216+
totalWallets: res.totalCount,
217+
});
207218
consecutiveFailures = 0; // Reset on success
208219

209220
if (!res.hasMore) {

0 commit comments

Comments
 (0)