diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts index 1401786d28d..7615e4766ed 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts @@ -435,7 +435,7 @@ export function useUserOpUsageAggregate(args: { const { clientId, from, to } = args; const { user, isLoggedIn } = useLoggedInUser(); - return useQuery({ + return useQuery({ queryKey: accountKeys.userOpStats( user?.address as string, clientId as string, @@ -444,12 +444,28 @@ export function useUserOpUsageAggregate(args: { "all", ), queryFn: async () => { - return getUserOpUsage({ + const userOpStats: UserOpStats[] = await getUserOpUsage({ clientId, from, to, period: "all", }); + + // Aggregate stats across wallet types + return userOpStats.reduce( + (acc, curr) => { + acc.successful += curr.successful; + acc.failed += curr.failed; + acc.sponsoredUsd += curr.sponsoredUsd; + return acc; + }, + { + date: (from || new Date()).toISOString(), + successful: 0, + failed: 0, + sponsoredUsd: 0, + }, + ); }, enabled: !!clientId && !!user?.address && isLoggedIn, }); diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx index 7f07be4839e..36555657151 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx @@ -44,7 +44,7 @@ export default async function Page(props: { ); return ( -
+
@@ -68,7 +68,9 @@ export default async function Page(props: { - +
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx index c1aeb8f4322..28f4bae7f37 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx @@ -10,10 +10,10 @@ import { } from "@3rdweb-sdk/react/hooks/useApi"; import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser"; import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts"; +import { SmartWallets } from "components/smart-wallets"; import { CircleAlertIcon } from "lucide-react"; import { useMemo } from "react"; import { useActiveWalletChain } from "thirdweb/react"; -import { AccountFactories } from "../../../../../../components/smart-wallets/AccountFactories"; import { AAFooterSection } from "./AAFooterSection"; import { isOpChainId } from "./isOpChain"; @@ -22,7 +22,11 @@ const TRACKING_CATEGORY = "smart-wallet"; // TODO - the factories shown on this page is not project specific, need to revamp this page export function AccountAbstractionPage(props: { + projectSlug: string; + teamSlug: string; + projectKey: string; apiKeyServices: ApiKeyService[]; + tab?: string; }) { const { apiKeyServices } = props; const looggedInUserQuery = useLoggedInUser(); @@ -48,7 +52,6 @@ export function AccountAbstractionPage(props: {

Account Abstraction

-

Easily integrate Account abstraction (ERC-4337) compliant smart accounts into your apps.{" "} @@ -62,9 +65,7 @@ export function AccountAbstractionPage(props: { View Documentation

-
- {looggedInUserQuery.isPending ? (
@@ -89,10 +90,15 @@ export function AccountAbstractionPage(props: { ) )} - +
)} -
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx index 1e83b8e3cd1..231aa14f011 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx @@ -6,6 +6,7 @@ import { AccountAbstractionPage } from "./AccountAbstractionPage"; export default async function Page(props: { params: { team_slug: string; project_slug: string }; + searchParams: { tab?: string }; }) { const { team_slug, project_slug } = props.params; const project = await getProject(team_slug, project_slug); @@ -22,7 +23,13 @@ export default async function Page(props: { return ( - + ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx index de28f942af4..f5bcd0ee0d1 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx @@ -1,24 +1,22 @@ "use client"; -import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { useUserOpUsageAggregate, - useUserOpUsagePeriod, useWalletUsageAggregate, useWalletUsagePeriod, } from "@3rdweb-sdk/react/hooks/useApi"; -import { differenceInDays, format, subDays } from "date-fns"; +import { + DateRangeSelector, + type Range, + getLastNDaysRange, +} from "components/analytics/date-range-selector"; +import { IntervalSelector } from "components/analytics/interval-selector"; +import { differenceInDays } from "date-fns"; import { useState } from "react"; import { ConnectAnalyticsDashboardUI } from "./ConnectAnalyticsDashboardUI"; export function ConnectAnalyticsDashboard(props: { clientId: string; + connectLayoutSlug: string; }) { const [range, setRange] = useState(() => getLastNDaysRange("last-120"), @@ -26,7 +24,6 @@ export function ConnectAnalyticsDashboard(props: { // use date-fns to calculate the number of days in the range const daysInRange = differenceInDays(range.to, range.from); - const [intervalType, setIntervalType] = useState<"day" | "week">( daysInRange > 30 ? "week" : "day", ); @@ -44,17 +41,8 @@ export function ConnectAnalyticsDashboard(props: { to: range.to, }); - const userOpUsageQuery = useUserOpUsagePeriod({ - clientId: props.clientId, - from: range.from, - to: range.to, - period: intervalType, - }); - - const userOpUsageAggregateQuery = useUserOpUsageAggregate({ + const userOpAggregateQuery = useUserOpUsageAggregate({ clientId: props.clientId, - from: range.from, - to: range.to, }); return ( @@ -77,8 +65,8 @@ export function ConnectAnalyticsDashboard(props: { ); } - -const durationPresets = [ - { - name: "Last 7 Days", - id: "last-7", - days: 7, - }, - { - name: "Last 30 Days", - id: "last-30", - days: 30, - }, - { - name: "Last 60 Days", - id: "last-60", - days: 60, - }, - { - name: "Last 120 Days", - id: "last-120", - days: 120, - }, -] as const; - -type DurationId = (typeof durationPresets)[number]["id"]; - -type Range = { - type: DurationId | "custom"; - label?: string; - from: Date; - to: Date; -}; - -function getLastNDaysRange(id: DurationId) { - const durationInfo = durationPresets.find((preset) => preset.id === id); - if (!durationInfo) { - throw new Error("Invalid duration id"); - } - - const todayDate = new Date(); - - const value: Range = { - type: id, - from: subDays(todayDate, durationInfo.days), - to: todayDate, - label: durationInfo.name, - }; - - return value; -} - -function DateRangeSelector(props: { - range: Range; - setRange: (range: Range) => void; -}) { - const { range, setRange } = props; - - return ( - - setRange({ - from, - to: range.to, - type: "custom", - }) - } - setTo={(to) => - setRange({ - from: range.from, - to, - type: "custom", - }) - } - header={ -
- -
- } - labelOverride={range.label} - className="w-auto" - /> - ); -} - -function IntervalSelector(props: { - intervalType: "day" | "week"; - setIntervalType: (intervalType: "day" | "week") => void; -}) { - return ( - - ); -} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboardUI.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboardUI.tsx index bbe3d02140b..623810123f9 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboardUI.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboardUI.tsx @@ -1,23 +1,19 @@ +import { Button } from "@/components/ui/button"; import type { UserOpStats, WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; -import { - ActivityIcon, - CableIcon, - CoinsIcon, - WalletCardsIcon, -} from "lucide-react"; -import type React from "react"; +import { Stat } from "components/analytics/stat"; +import { AccountAbstractionSummary } from "components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary"; +import { CableIcon, WalletCardsIcon } from "lucide-react"; +import Link from "next/link"; 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[]; + aggregateUserOpUsageQuery?: UserOpStats; + connectLayoutSlug: string; isPending: boolean; }) { const { totalWallets, uniqueWallets } = useMemo(() => { @@ -31,17 +27,6 @@ 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 (
{/* Connections */} @@ -54,6 +39,38 @@ export function ConnectAnalyticsDashboardUI(props: { />
+
+
+
+ +
+
+
+

+ Account Abstraction +

+

+ Sponsor and batch transactions for your users. +

+
+ +
+ +
+
+ +
+ +
+
+
+ - - {/* Connections */} -
- - - new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(value) - } - icon={CoinsIcon} - /> -
- - - -
); } - -const Stat: React.FC<{ - label: string; - value?: number; - icon: React.FC<{ className?: string }>; - formatter?: (value: number) => string; -}> = ({ label, value, formatter, icon: Icon }) => { - return ( -
-
-
- {value && formatter ? formatter(value) : value?.toLocaleString()} -
-
- {label} -
-
- -
- ); -}; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectAnalyticsDashboard.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectAnalyticsDashboard.stories.tsx index 53293795993..31a4eae5282 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectAnalyticsDashboard.stories.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectAnalyticsDashboard.stories.tsx @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { createUserOpStatsStub } from "components/smart-wallets/AccountAbstractionAnalytics/storyUtils"; import { mobileViewport } from "../../../../../../../stories/utils"; import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI"; -import { createUserOpStatsStub, createWalletStatsStub } from "./storyUtils"; +import { createWalletStatsStub } from "./storyUtils"; const meta = { title: "Charts/Connect/Analytics Dashboard", @@ -31,8 +32,8 @@ function Component() {
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx index 8c270d01654..b8957cf9eec 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx @@ -15,6 +15,10 @@ import { SelectValue, } from "@/components/ui/select"; import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { + EmptyChartState, + LoadingChartState, +} from "components/analytics/empty-chart-state"; import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon"; import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; @@ -24,7 +28,6 @@ import { DocLink } from "components/shared/DocLink"; import { format } from "date-fns"; import { useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts"; -import { EmptyChartState, LoadingChartState } from "./EmptyChartState"; type ChartToShow = "uniqueWallets" | "totalWallets"; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletConnectorsChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletConnectorsChartCard.tsx index e4202b5a1d0..eeaa8be0380 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletConnectorsChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletConnectorsChartCard.tsx @@ -17,13 +17,16 @@ import { SelectValue, } from "@/components/ui/select"; import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { + EmptyChartState, + LoadingChartState, +} from "components/analytics/empty-chart-state"; import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; import { DocLink } from "components/shared/DocLink"; import { format } from "date-fns"; import { useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; -import { EmptyChartState, LoadingChartState } from "./EmptyChartState"; type ChartToShow = "uniqueWalletsConnected" | "totalConnections"; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletDistributionChartCard.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletDistributionChartCard.tsx index 5d989be7947..53ea056ad4f 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletDistributionChartCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/WalletDistributionChartCard.tsx @@ -17,12 +17,15 @@ import { SelectValue, } from "@/components/ui/select"; import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { + EmptyChartState, + LoadingChartState, +} from "components/analytics/empty-chart-state"; import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; import { DocLink } from "components/shared/DocLink"; import { useMemo, useState } from "react"; import { Pie, PieChart } from "recharts"; -import { EmptyChartState, LoadingChartState } from "./EmptyChartState"; type ChartToShow = "totalConnections" | "uniqueWalletsConnected"; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/storyUtils.ts b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/storyUtils.ts index 732eb87f327..401deb97b0c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/storyUtils.ts +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/storyUtils.ts @@ -1,4 +1,4 @@ -import type { UserOpStats, WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi"; import type { WalletId } from "thirdweb/wallets"; const walletsToPickFrom: WalletId[] = [ @@ -50,26 +50,3 @@ export function createWalletStatsStub(days: number): WalletStats[] { return stubbedData; } - -export function createUserOpStatsStub(days: number): UserOpStats[] { - const stubbedData: UserOpStats[] = []; - - let d = days; - while (d !== 0) { - const successful = Math.floor(Math.random() * 100); - const failed = Math.floor(Math.random() * 100); - const sponsoredUsd = Math.floor(Math.random() * 100); - stubbedData.push({ - date: new Date(2024, 1, d).toLocaleString(), - successful, - failed, - sponsoredUsd, - }); - - if (Math.random() > 0.7) { - d--; - } - } - - return stubbedData; -} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/page.tsx index f5b02c8127f..ef6ee1d64ba 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/page.tsx @@ -30,7 +30,10 @@ export default async function Page(props: {
- +
diff --git a/apps/dashboard/src/components/analytics/date-range-selector.tsx b/apps/dashboard/src/components/analytics/date-range-selector.tsx new file mode 100644 index 00000000000..65841f7607c --- /dev/null +++ b/apps/dashboard/src/components/analytics/date-range-selector.tsx @@ -0,0 +1,117 @@ +import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { format, subDays } from "date-fns"; + +export function DateRangeSelector(props: { + range: Range; + setRange: (range: Range) => void; +}) { + const { range, setRange } = props; + + return ( + + setRange({ + from, + to: range.to, + type: "custom", + }) + } + setTo={(to) => + setRange({ + from: range.from, + to, + type: "custom", + }) + } + header={ +
+ +
+ } + labelOverride={range.label} + className="w-auto" + /> + ); +} + +export function getLastNDaysRange(id: DurationId) { + const durationInfo = durationPresets.find((preset) => preset.id === id); + if (!durationInfo) { + throw new Error("Invalid duration id"); + } + + const todayDate = new Date(); + + const value: Range = { + type: id, + from: subDays(todayDate, durationInfo.days), + to: todayDate, + label: durationInfo.name, + }; + + return value; +} + +const durationPresets = [ + { + name: "Last 7 Days", + id: "last-7", + days: 7, + }, + { + name: "Last 30 Days", + id: "last-30", + days: 30, + }, + { + name: "Last 60 Days", + id: "last-60", + days: 60, + }, + { + name: "Last 120 Days", + id: "last-120", + days: 120, + }, +] as const; + +type DurationId = (typeof durationPresets)[number]["id"]; + +export type Range = { + type: DurationId | "custom"; + label?: string; + from: Date; + to: Date; +}; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/EmptyChartState.tsx b/apps/dashboard/src/components/analytics/empty-chart-state.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/EmptyChartState.tsx rename to apps/dashboard/src/components/analytics/empty-chart-state.tsx diff --git a/apps/dashboard/src/components/analytics/interval-selector.tsx b/apps/dashboard/src/components/analytics/interval-selector.tsx new file mode 100644 index 00000000000..70a3bced689 --- /dev/null +++ b/apps/dashboard/src/components/analytics/interval-selector.tsx @@ -0,0 +1,29 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function IntervalSelector(props: { + intervalType: "day" | "week"; + setIntervalType: (intervalType: "day" | "week") => void; +}) { + return ( + + ); +} diff --git a/apps/dashboard/src/components/analytics/stat.tsx b/apps/dashboard/src/components/analytics/stat.tsx new file mode 100644 index 00000000000..4bee8f9e56a --- /dev/null +++ b/apps/dashboard/src/components/analytics/stat.tsx @@ -0,0 +1,20 @@ +export const Stat: React.FC<{ + label: string; + value?: number; + icon: React.FC<{ className?: string }>; + formatter?: (value: number) => string; +}> = ({ label, value, formatter, icon: Icon }) => { + return ( +
+
+
+ {value && formatter ? formatter(value) : value?.toLocaleString()} +
+
+ {label} +
+
+ +
+ ); +}; diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary.tsx new file mode 100644 index 00000000000..c90dcec9318 --- /dev/null +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary.tsx @@ -0,0 +1,28 @@ +import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { Stat } from "components/analytics/stat"; +import { ActivityIcon, CoinsIcon } from "lucide-react"; + +export function AccountAbstractionSummary(props: { + aggregateUserOpUsageQuery?: UserOpStats; +}) { + return ( +
+ + + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(value) + } + icon={CoinsIcon} + /> +
+ ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.stories.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.stories.tsx similarity index 94% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.stories.tsx rename to apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.stories.tsx index 2fb321c7173..217bd8cbe7a 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.stories.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.stories.tsx @@ -1,8 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { - BadgeContainer, - mobileViewport, -} from "../../../../../../../stories/utils"; +import { BadgeContainer, mobileViewport } from "../../../stories/utils"; import { SponsoredTransactionsChartCard } from "./SponsoredTransactionsChartCard"; import { createUserOpStatsStub } from "./storyUtils"; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx similarity index 95% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.tsx rename to apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx index c42bfc91d42..463d86bd3a2 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/SponsoredTransactionsChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard.tsx @@ -9,6 +9,10 @@ import { ChartTooltipContent, } from "@/components/ui/chart"; import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { + EmptyChartState, + LoadingChartState, +} from "components/analytics/empty-chart-state"; import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon"; import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; @@ -18,7 +22,6 @@ import { DocLink } from "components/shared/DocLink"; import { format } from "date-fns"; import { useMemo } from "react"; import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; -import { EmptyChartState, LoadingChartState } from "./EmptyChartState"; type ChartData = { time: string; // human readable date @@ -47,13 +50,13 @@ export function SponsoredTransactionsChartCard(props: { for (const data of userOpStats) { const chartData = chartDataMap.get(data.date); - if (!chartData && data.sponsoredUsd > 0) { + if (!chartData) { chartDataMap.set(data.date, { time: format(new Date(data.date), "MMM dd"), successful: data.successful, failed: data.failed, }); - } else if (chartData && data.sponsoredUsd > 0) { + } else { chartData.successful += data.successful; chartData.failed += data.failed; } @@ -90,7 +93,10 @@ export function SponsoredTransactionsChartCard(props: { {props.isPending ? ( - ) : barChartData.length === 0 ? ( + ) : barChartData.length === 0 || + barChartData.every( + (data) => data.failed === 0 && data.successful === 0, + ) ? (
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.stories.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.stories.tsx similarity index 94% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.stories.tsx rename to apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.stories.tsx index 842430d2c29..ff665d0aa26 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.stories.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.stories.tsx @@ -1,8 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { - BadgeContainer, - mobileViewport, -} from "../../../../../../../stories/utils"; +import { BadgeContainer, mobileViewport } from "../../../stories/utils"; import { TotalSponsoredChartCard } from "./TotalSponsoredChartCard"; import { createUserOpStatsStub } from "./storyUtils"; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx similarity index 95% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.tsx rename to apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx index c31a36831f1..6a1806a6e5e 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/TotalSponsoredChartCard.tsx +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/TotalSponsoredChartCard.tsx @@ -8,6 +8,10 @@ import { ChartTooltipContent, } from "@/components/ui/chart"; import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { + EmptyChartState, + LoadingChartState, +} from "components/analytics/empty-chart-state"; import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon"; import { ReactIcon } from "components/icons/brand-icons/ReactIcon"; import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon"; @@ -17,7 +21,6 @@ import { DocLink } from "components/shared/DocLink"; import { format } from "date-fns"; import { useMemo } from "react"; import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts"; -import { EmptyChartState, LoadingChartState } from "./EmptyChartState"; type ChartData = { time: string; // human readable date @@ -41,12 +44,12 @@ export function TotalSponsoredChartCard(props: { for (const data of userOpStats) { const chartData = chartDataMap.get(data.date); - if (!chartData && data.sponsoredUsd > 0) { + if (!chartData) { chartDataMap.set(data.date, { time: format(new Date(data.date), "MMM dd"), sponsoredUsd: data.sponsoredUsd, }); - } else if (chartData && data.sponsoredUsd > 0) { + } else { chartData.sponsoredUsd += data.sponsoredUsd; } } @@ -88,7 +91,8 @@ export function TotalSponsoredChartCard(props: { > {props.isPending ? ( - ) : barChartData.length === 0 ? ( + ) : barChartData.length === 0 || + barChartData.every((data) => data.sponsoredUsd === 0) ? (
Sponsor gas for your users diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx new file mode 100644 index 00000000000..72cc50958aa --- /dev/null +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/index.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useUserOpUsagePeriod } from "@3rdweb-sdk/react/hooks/useApi"; +import { + DateRangeSelector, + type Range, + getLastNDaysRange, +} from "components/analytics/date-range-selector"; +import { IntervalSelector } from "components/analytics/interval-selector"; +import { differenceInDays } from "date-fns"; +import { useState } from "react"; +import { SponsoredTransactionsChartCard } from "./SponsoredTransactionsChartCard"; +import { TotalSponsoredChartCard } from "./TotalSponsoredChartCard"; + +export function AccountAbstractionAnalytics({ + clientId, +}: { clientId: string }) { + const [range, setRange] = useState(() => + getLastNDaysRange("last-120"), + ); + + // use date-fns to calculate the number of days in the range + const daysInRange = differenceInDays(range.to, range.from); + const [intervalType, setIntervalType] = useState<"day" | "week">( + daysInRange > 30 ? "week" : "day", + ); + + const userOpUsageQuery = useUserOpUsagePeriod({ + clientId: clientId, + from: range.from, + to: range.to, + period: intervalType, + }); + + return ( +
+
+ { + setRange(newRange); + const days = differenceInDays(newRange.to, newRange.from); + setIntervalType(days > 30 ? "week" : "day"); + }} + /> + +
+ +
+ +
+ + + +
+
+ ); +} diff --git a/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts new file mode 100644 index 00000000000..a7b2824d04d --- /dev/null +++ b/apps/dashboard/src/components/smart-wallets/AccountAbstractionAnalytics/storyUtils.ts @@ -0,0 +1,24 @@ +import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi"; + +export function createUserOpStatsStub(days: number): UserOpStats[] { + const stubbedData: UserOpStats[] = []; + + let d = days; + while (d !== 0) { + const successful = Math.floor(Math.random() * 100); + const failed = Math.floor(Math.random() * 100); + const sponsoredUsd = Math.floor(Math.random() * 100); + stubbedData.push({ + date: new Date(2024, 1, d).toLocaleString(), + successful, + failed, + sponsoredUsd, + }); + + if (Math.random() > 0.7) { + d--; + } + } + + return stubbedData; +} diff --git a/apps/dashboard/src/components/smart-wallets/index.tsx b/apps/dashboard/src/components/smart-wallets/index.tsx index ae05d4eb08b..5928f20f1e1 100644 --- a/apps/dashboard/src/components/smart-wallets/index.tsx +++ b/apps/dashboard/src/components/smart-wallets/index.tsx @@ -1,52 +1,76 @@ "use client"; -import { TabButtons } from "@/components/ui/tabs"; -import type { ApiKeyService } from "@3rdweb-sdk/react/hooks/useApi"; -import { useState } from "react"; +import { TabLinks } from "@/components/ui/tabs"; +import { + type ApiKeyService, + useUserOpUsageAggregate, +} from "@3rdweb-sdk/react/hooks/useApi"; +import { AccountAbstractionAnalytics } from "./AccountAbstractionAnalytics"; +import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary"; import { AccountFactories } from "./AccountFactories"; import { AccountAbstractionSettingsPage } from "./SponsorshipPolicies"; interface SmartWalletsProps { apiKeyServices: ApiKeyService[]; trackingCategory: string; - defaultTab: 0 | 1; + clientId: string; + smartWalletsLayoutSlug: string; + tab?: string; } export const SmartWallets: React.FC = ({ apiKeyServices, trackingCategory, - defaultTab, + clientId, + smartWalletsLayoutSlug, + tab = "analytics", }) => { - const [selectedTab, setSelectedTab] = useState<"factories" | "config">( - defaultTab === 0 ? "config" : "factories", - ); + const aggregateUserOpUsageQuery = useUserOpUsageAggregate({ + clientId, + }); return (
- + +
+ + setSelectedTab("config"), - isActive: selectedTab === "config", - isEnabled: true, + href: `${smartWalletsLayoutSlug}?tab=config`, + isActive: tab === "config", + isDisabled: false, }, { name: "Account Factories", - onClick: () => setSelectedTab("factories"), - isActive: selectedTab === "factories", - isEnabled: true, + href: `${smartWalletsLayoutSlug}?tab=factories`, + isActive: tab === "factories", + isDisabled: false, }, ]} />
- {selectedTab === "factories" && ( + {tab === "analytics" && ( + + )} + + {tab === "factories" && ( )} - {selectedTab === "config" && ( + {tab === "config" && (