Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ export const accountKeys = {
to,
period,
] as const,
userOpStats: (
walletAddress: string,
clientId: string,
from: string,
to: string,
period: string,
) =>
[
...accountKeys.wallet(walletAddress),
"userOps",
clientId,
from,
to,
period,
] as const,
credits: (walletAddress: string) =>
[...accountKeys.wallet(walletAddress), "credits"] as const,
billingSession: (walletAddress: string) =>
Expand Down
101 changes: 101 additions & 0 deletions apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ export interface WalletStats {
walletType: string;
}

export interface UserOpStats {
date: string;
successful: number;
failed: number;
sponsoredUsd: number;
}

interface BillingProduct {
name: string;
id: string;
Expand Down Expand Up @@ -383,6 +390,100 @@ async function getWalletUsage(args: {
return json.data;
}

async function getUserOpUsage(args: {
clientId: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const { clientId, from, to, period } = args;

const searchParams = new URLSearchParams();
searchParams.append("clientId", clientId);
if (from) {
searchParams.append("from", from.toISOString());
}
if (to) {
searchParams.append("to", to.toISOString());
}
if (period) {
searchParams.append("period", period);
}
const res = await fetch(
`${THIRDWEB_ANALYTICS_API_HOST}/v1/user-ops?${searchParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
const json = await res.json();

if (res.status !== 200) {
throw new Error(json.message);
}

return json.data;
}

export function useUserOpUsageAggregate(args: {
clientId: string;
from?: Date;
to?: Date;
}) {
const { clientId, from, to } = args;
const { user, isLoggedIn } = useLoggedInUser();

return useQuery({
queryKey: accountKeys.userOpStats(
user?.address as string,
clientId as string,
from?.toISOString() || "",
to?.toISOString() || "",
"all",
),
queryFn: async () => {
return getUserOpUsage({
clientId,
from,
to,
period: "all",
});
},
enabled: !!clientId && !!user?.address && isLoggedIn,
});
}

export function useUserOpUsagePeriod(args: {
clientId: string;
from?: Date;
to?: Date;
period: "day" | "week" | "month" | "year";
}) {
const { clientId, from, to, period } = args;
const { user, isLoggedIn } = useLoggedInUser();

return useQuery({
queryKey: accountKeys.userOpStats(
user?.address as string,
clientId as string,
from?.toISOString() || "",
to?.toISOString() || "",
period,
),
queryFn: async () => {
return getUserOpUsage({
clientId,
from,
to,
period,
});
},
enabled: !!clientId && !!user?.address && isLoggedIn,
});
}

export function useWalletUsageAggregate(args: {
clientId: string;
from?: Date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import {
useUserOpUsageAggregate,
useUserOpUsagePeriod,
useWalletUsageAggregate,
useWalletUsagePeriod,
} from "@3rdweb-sdk/react/hooks/useApi";
Expand Down Expand Up @@ -42,6 +44,19 @@ export function ConnectAnalyticsDashboard(props: {
to: range.to,
});

const userOpUsageQuery = useUserOpUsagePeriod({
clientId: props.clientId,
from: range.from,
to: range.to,
period: intervalType,
});

const userOpUsageAggregateQuery = useUserOpUsageAggregate({
clientId: props.clientId,
from: range.from,
to: range.to,
});

return (
<div>
<div className="flex gap-3">
Expand All @@ -62,6 +77,8 @@ export function ConnectAnalyticsDashboard(props: {
<ConnectAnalyticsDashboardUI
walletUsage={walletUsageQuery.data || []}
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
userOpUsage={userOpUsageQuery.data || []}
aggregateUserOpUsage={userOpUsageAggregateQuery.data || []}
isPending={
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
import { CableIcon, WalletCardsIcon } from "lucide-react";
import type { UserOpStats, WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
import {
ActivityIcon,
CableIcon,
CoinsIcon,
WalletCardsIcon,
} from "lucide-react";
import type React from "react";
import { useMemo } from "react";
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
import { SponsoredTransactionsChartCard } from "./_components/SponsoredTransactionsChartCard";
import { TotalSponsoredChartCard } from "./_components/TotalSponsoredChartCard";
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";

export function ConnectAnalyticsDashboardUI(props: {
walletUsage: WalletStats[];
aggregateWalletUsage: WalletStats[];
userOpUsage: UserOpStats[];
aggregateUserOpUsage: UserOpStats[];
isPending: boolean;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
Expand All @@ -22,9 +31,20 @@ export function ConnectAnalyticsDashboardUI(props: {
);
}, [props.aggregateWalletUsage]);

const { totalSponsoredTransactions, totalSponsoredUsd } = useMemo(() => {
return props.aggregateUserOpUsage.reduce(
(acc, curr) => {
acc.totalSponsoredTransactions += curr.successful;
acc.totalSponsoredUsd += curr.sponsoredUsd;
return acc;
},
{ totalSponsoredTransactions: 0, totalSponsoredUsd: 0 },
);
}, [props.aggregateUserOpUsage]);

return (
<div className="flex flex-col gap-4 lg:gap-6">
{/* Summary Stat Cards */}
{/* Connections */}
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
<Stat
Expand All @@ -48,6 +68,36 @@ export function ConnectAnalyticsDashboardUI(props: {
walletStats={props.walletUsage}
isPending={props.isPending}
/>

{/* Connections */}
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat
label="Sponsored Transactions"
value={totalSponsoredTransactions}
icon={ActivityIcon}
/>
<Stat
label="Total Sponsored"
value={totalSponsoredUsd}
formatter={(value) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value)
}
icon={CoinsIcon}
/>
</div>

<TotalSponsoredChartCard
userOpStats={props.userOpUsage}
isPending={props.isPending}
/>

<SponsoredTransactionsChartCard
userOpStats={props.userOpUsage}
isPending={props.isPending}
/>
</div>
);
}
Expand All @@ -56,12 +106,13 @@ const Stat: React.FC<{
label: string;
value?: number;
icon: React.FC<{ className?: string }>;
}> = ({ label, value, icon: Icon }) => {
formatter?: (value: number) => string;
}> = ({ label, value, formatter, icon: Icon }) => {
return (
<dl className="flex items-center justify-between gap-4 rounded-lg border border-border bg-muted/50 p-4 lg:p-6">
<div>
<dd className="font-semibold text-3xl tracking-tight lg:text-5xl">
{value?.toLocaleString()}
{value && formatter ? formatter(value) : value?.toLocaleString()}
</dd>
<dt className="font-medium text-muted-foreground text-sm tracking-tight lg:text-lg">
{label}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../../../../../stories/utils";
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
import { createWalletStatsStub } from "./storyUtils";
import { createUserOpStatsStub, createWalletStatsStub } from "./storyUtils";

const meta = {
title: "Charts/Connect/Analytics Dashboard",
Expand Down Expand Up @@ -31,6 +31,8 @@ function Component() {
<ConnectAnalyticsDashboardUI
walletUsage={createWalletStatsStub(30)}
aggregateWalletUsage={createWalletStatsStub(30)}
userOpUsage={createUserOpStatsStub(30)}
aggregateUserOpUsage={createUserOpStatsStub(1)}
isPending={false}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
import { createWalletStatsStub } from "./storyUtils";

const meta = {
title: "Charts/Connect/DailyConnections",
title: "Charts/Connect/Daily Connections",
component: Component,
parameters: {
layout: "centered",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function DailyConnectionsChartCard(props: {
]);
return { header, rows };
}}
fileName="DialyConnections"
fileName="DailyConnections"
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { SponsoredTransactionsChartCard } from "./SponsoredTransactionsChartCard";
import { createUserOpStatsStub } from "./storyUtils";

const meta = {
title: "Charts/Connect/Sponsored Transactions",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noice

component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Desktop: Story = {
args: {},
};

export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};

function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<SponsoredTransactionsChartCard
userOpStats={createUserOpStatsStub(30)}
isPending={false}
/>
</BadgeContainer>

<BadgeContainer label="60 days">
<SponsoredTransactionsChartCard
userOpStats={createUserOpStatsStub(60)}
isPending={false}
/>
</BadgeContainer>

<BadgeContainer label="10 days">
<SponsoredTransactionsChartCard
userOpStats={createUserOpStatsStub(10)}
isPending={false}
/>
</BadgeContainer>

<BadgeContainer label="0 days">
<SponsoredTransactionsChartCard
userOpStats={createUserOpStatsStub(0)}
isPending={false}
/>
</BadgeContainer>

<BadgeContainer label="Loading">
<SponsoredTransactionsChartCard
userOpStats={createUserOpStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
Loading
Loading