Skip to content

Commit c04b93e

Browse files
committed
[Dashboard] Feature: AA analytics tab (#5149)
<!-- start pr-codex --> ## PR-Codex overview This PR focuses on enhancing the analytics dashboard for account abstraction by updating components, improving data handling, and adding new features for user operation statistics and wallet usage. ### Detailed summary - Updated `clientId` handling in `ConnectAnalyticsDashboard` and `AccountAbstractionPage`. - Introduced `connectLayoutSlug` prop in `ConnectAnalyticsDashboard`. - Added `IntervalSelector` and `DateRangeSelector` components for better date management. - Refactored `ConnectAnalyticsDashboardUI` to include `AccountAbstractionSummary`. - Enhanced `TotalSponsoredChartCard` and `SponsoredTransactionsChartCard` with new chart states. - Moved `createUserOpStatsStub` to a new location and cleaned up unused code. - Improved data aggregation for user operation statistics in `useUserOpUsageAggregate`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent e6e9944 commit c04b93e

25 files changed

+464
-303
lines changed

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ export function useUserOpUsageAggregate(args: {
435435
const { clientId, from, to } = args;
436436
const { user, isLoggedIn } = useLoggedInUser();
437437

438-
return useQuery({
438+
return useQuery<UserOpStats>({
439439
queryKey: accountKeys.userOpStats(
440440
user?.address as string,
441441
clientId as string,
@@ -444,12 +444,28 @@ export function useUserOpUsageAggregate(args: {
444444
"all",
445445
),
446446
queryFn: async () => {
447-
return getUserOpUsage({
447+
const userOpStats: UserOpStats[] = await getUserOpUsage({
448448
clientId,
449449
from,
450450
to,
451451
period: "all",
452452
});
453+
454+
// Aggregate stats across wallet types
455+
return userOpStats.reduce(
456+
(acc, curr) => {
457+
acc.successful += curr.successful;
458+
acc.failed += curr.failed;
459+
acc.sponsoredUsd += curr.sponsoredUsd;
460+
return acc;
461+
},
462+
{
463+
date: (from || new Date()).toISOString(),
464+
successful: 0,
465+
failed: 0,
466+
sponsoredUsd: 0,
467+
},
468+
);
453469
},
454470
enabled: !!clientId && !!user?.address && isLoggedIn,
455471
});

apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default async function Page(props: {
4444
);
4545

4646
return (
47-
<div className="flex flex-col gap-10">
47+
<div className="flex flex-col gap-6">
4848
<div className="flex flex-col content-start justify-between gap-4 lg:flex-row">
4949
<PageHeader />
5050

@@ -68,7 +68,9 @@ export default async function Page(props: {
6868
<SmartWallets
6969
apiKeyServices={apiKey.services || []}
7070
trackingCategory="smart-wallet"
71-
defaultTab={props.searchParams.tab === "1" ? 1 : 0}
71+
tab={props.searchParams.tab}
72+
smartWalletsLayoutSlug={`/dashboard/connect/account-abstraction/${clientId}`}
73+
clientId={apiKey.key}
7274
/>
7375

7476
<ConnectSDKCard

apps/dashboard/src/app/(dashboard)/dashboard/connect/analytics/[clientId]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ export default async function Page(props: {
5151

5252
<div className="h-4 lg:h-8" />
5353

54-
<ConnectAnalyticsDashboard clientId={apiKey.key} />
54+
<ConnectAnalyticsDashboard
55+
clientId={apiKey.key}
56+
connectLayoutSlug="/dashboard/connect"
57+
/>
5558

5659
<div className="h-4 lg:h-8" />
5760
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
} from "@3rdweb-sdk/react/hooks/useApi";
1111
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
1212
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
13+
import { SmartWallets } from "components/smart-wallets";
1314
import { CircleAlertIcon } from "lucide-react";
1415
import { useMemo } from "react";
1516
import { useActiveWalletChain } from "thirdweb/react";
16-
import { AccountFactories } from "../../../../../../components/smart-wallets/AccountFactories";
1717
import { AAFooterSection } from "./AAFooterSection";
1818
import { isOpChainId } from "./isOpChain";
1919

@@ -22,7 +22,11 @@ const TRACKING_CATEGORY = "smart-wallet";
2222
// TODO - the factories shown on this page is not project specific, need to revamp this page
2323

2424
export function AccountAbstractionPage(props: {
25+
projectSlug: string;
26+
teamSlug: string;
27+
projectKey: string;
2528
apiKeyServices: ApiKeyService[];
29+
tab?: string;
2630
}) {
2731
const { apiKeyServices } = props;
2832
const looggedInUserQuery = useLoggedInUser();
@@ -48,7 +52,6 @@ export function AccountAbstractionPage(props: {
4852
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
4953
Account Abstraction
5054
</h1>
51-
5255
<p className="text-muted-foreground text-sm">
5356
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
5457
into your apps.{" "}
@@ -62,9 +65,7 @@ export function AccountAbstractionPage(props: {
6265
View Documentation
6366
</TrackedLinkTW>
6467
</p>
65-
6668
<div className="h-6" />
67-
6869
{looggedInUserQuery.isPending ? (
6970
<div className="flex h-[400px] items-center justify-center rounded-lg border border-border">
7071
<Spinner className="size-14" />
@@ -89,10 +90,15 @@ export function AccountAbstractionPage(props: {
8990
)
9091
)}
9192

92-
<AccountFactories trackingCategory={TRACKING_CATEGORY} />
93+
<SmartWallets
94+
smartWalletsLayoutSlug={`/team/${props.teamSlug}/${props.projectSlug}/connect/account-abstraction`}
95+
apiKeyServices={apiKeyServices}
96+
trackingCategory={TRACKING_CATEGORY}
97+
clientId={props.projectKey}
98+
tab={props.tab}
99+
/>
93100
</div>
94101
)}
95-
96102
<div className="h-14" />
97103
<AAFooterSection trackingCategory={TRACKING_CATEGORY} />
98104
</div>

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AccountAbstractionPage } from "./AccountAbstractionPage";
66

77
export default async function Page(props: {
88
params: { team_slug: string; project_slug: string };
9+
searchParams: { tab?: string };
910
}) {
1011
const { team_slug, project_slug } = props.params;
1112
const project = await getProject(team_slug, project_slug);
@@ -22,7 +23,13 @@ export default async function Page(props: {
2223

2324
return (
2425
<ChakraProviderSetup>
25-
<AccountAbstractionPage apiKeyServices={apiKey.services || []} />
26+
<AccountAbstractionPage
27+
projectSlug={project.slug}
28+
teamSlug={team_slug}
29+
projectKey={project.publishableKey}
30+
apiKeyServices={apiKey.services || []}
31+
tab={props.searchParams.tab}
32+
/>
2633
</ChakraProviderSetup>
2734
);
2835
}
Lines changed: 11 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
11
"use client";
2-
import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange";
3-
import {
4-
Select,
5-
SelectContent,
6-
SelectItem,
7-
SelectTrigger,
8-
SelectValue,
9-
} from "@/components/ui/select";
102
import {
113
useUserOpUsageAggregate,
12-
useUserOpUsagePeriod,
134
useWalletUsageAggregate,
145
useWalletUsagePeriod,
156
} from "@3rdweb-sdk/react/hooks/useApi";
16-
import { differenceInDays, format, subDays } from "date-fns";
7+
import {
8+
DateRangeSelector,
9+
type Range,
10+
getLastNDaysRange,
11+
} from "components/analytics/date-range-selector";
12+
import { IntervalSelector } from "components/analytics/interval-selector";
13+
import { differenceInDays } from "date-fns";
1714
import { useState } from "react";
1815
import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI";
1916

2017
export function ConnectAnalyticsDashboard(props: {
2118
clientId: string;
19+
connectLayoutSlug: string;
2220
}) {
2321
const [range, setRange] = useState<Range>(() =>
2422
getLastNDaysRange("last-120"),
2523
);
2624

2725
// use date-fns to calculate the number of days in the range
2826
const daysInRange = differenceInDays(range.to, range.from);
29-
3027
const [intervalType, setIntervalType] = useState<"day" | "week">(
3128
daysInRange > 30 ? "week" : "day",
3229
);
@@ -44,17 +41,8 @@ export function ConnectAnalyticsDashboard(props: {
4441
to: range.to,
4542
});
4643

47-
const userOpUsageQuery = useUserOpUsagePeriod({
48-
clientId: props.clientId,
49-
from: range.from,
50-
to: range.to,
51-
period: intervalType,
52-
});
53-
54-
const userOpUsageAggregateQuery = useUserOpUsageAggregate({
44+
const userOpAggregateQuery = useUserOpUsageAggregate({
5545
clientId: props.clientId,
56-
from: range.from,
57-
to: range.to,
5846
});
5947

6048
return (
@@ -77,142 +65,12 @@ export function ConnectAnalyticsDashboard(props: {
7765
<ConnectAnalyticsDashboardUI
7866
walletUsage={walletUsageQuery.data || []}
7967
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
80-
userOpUsage={userOpUsageQuery.data || []}
81-
aggregateUserOpUsage={userOpUsageAggregateQuery.data || []}
68+
aggregateUserOpUsageQuery={userOpAggregateQuery.data}
69+
connectLayoutSlug={props.connectLayoutSlug}
8270
isPending={
8371
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
8472
}
8573
/>
8674
</div>
8775
);
8876
}
89-
90-
const durationPresets = [
91-
{
92-
name: "Last 7 Days",
93-
id: "last-7",
94-
days: 7,
95-
},
96-
{
97-
name: "Last 30 Days",
98-
id: "last-30",
99-
days: 30,
100-
},
101-
{
102-
name: "Last 60 Days",
103-
id: "last-60",
104-
days: 60,
105-
},
106-
{
107-
name: "Last 120 Days",
108-
id: "last-120",
109-
days: 120,
110-
},
111-
] as const;
112-
113-
type DurationId = (typeof durationPresets)[number]["id"];
114-
115-
type Range = {
116-
type: DurationId | "custom";
117-
label?: string;
118-
from: Date;
119-
to: Date;
120-
};
121-
122-
function getLastNDaysRange(id: DurationId) {
123-
const durationInfo = durationPresets.find((preset) => preset.id === id);
124-
if (!durationInfo) {
125-
throw new Error("Invalid duration id");
126-
}
127-
128-
const todayDate = new Date();
129-
130-
const value: Range = {
131-
type: id,
132-
from: subDays(todayDate, durationInfo.days),
133-
to: todayDate,
134-
label: durationInfo.name,
135-
};
136-
137-
return value;
138-
}
139-
140-
function DateRangeSelector(props: {
141-
range: Range;
142-
setRange: (range: Range) => void;
143-
}) {
144-
const { range, setRange } = props;
145-
146-
return (
147-
<DatePickerWithRange
148-
from={range.from}
149-
to={range.to}
150-
setFrom={(from) =>
151-
setRange({
152-
from,
153-
to: range.to,
154-
type: "custom",
155-
})
156-
}
157-
setTo={(to) =>
158-
setRange({
159-
from: range.from,
160-
to,
161-
type: "custom",
162-
})
163-
}
164-
header={
165-
<div className="mb-2 border-border border-b p-4">
166-
<Select
167-
value={range.type}
168-
onValueChange={(id: DurationId) => {
169-
setRange(getLastNDaysRange(id));
170-
}}
171-
>
172-
<SelectTrigger className="flex bg-transparent">
173-
<SelectValue placeholder="Select" />
174-
</SelectTrigger>
175-
<SelectContent position="popper">
176-
{durationPresets.map((preset) => (
177-
<SelectItem key={preset.id} value={preset.id}>
178-
{preset.name}
179-
</SelectItem>
180-
))}
181-
182-
{range.type === "custom" && (
183-
<SelectItem value="custom">
184-
{format(range.from, "LLL dd, y")} -{" "}
185-
{format(range.to, "LLL dd, y")}
186-
</SelectItem>
187-
)}
188-
</SelectContent>
189-
</Select>
190-
</div>
191-
}
192-
labelOverride={range.label}
193-
className="w-auto"
194-
/>
195-
);
196-
}
197-
198-
function IntervalSelector(props: {
199-
intervalType: "day" | "week";
200-
setIntervalType: (intervalType: "day" | "week") => void;
201-
}) {
202-
return (
203-
<Select
204-
value={props.intervalType}
205-
onValueChange={(value: "day" | "week") => {
206-
props.setIntervalType(value);
207-
}}
208-
>
209-
<SelectTrigger className="w-auto hover:bg-muted">
210-
<SelectValue placeholder="Select" />
211-
</SelectTrigger>
212-
<SelectContent position="popper">
213-
<SelectItem value="week"> Weekly </SelectItem>
214-
<SelectItem value="day"> Daily</SelectItem>
215-
</SelectContent>
216-
</Select>
217-
);
218-
}

0 commit comments

Comments
 (0)