diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts index 408ef187538..2d7d2a27b1d 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts @@ -244,6 +244,8 @@ export interface InAppWalletStats { uniqueWalletsConnected: number; } +export interface EcosystemWalletStats extends InAppWalletStats {} + export interface UserOpStats { date: string; successful: number; diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx new file mode 100644 index 00000000000..63a4179fa0c --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx @@ -0,0 +1,39 @@ +import { + type Range, + getLastNDaysRange, +} from "components/analytics/date-range-selector"; +import { RangeSelector } from "components/analytics/range-selector"; +import { getEcosystemWalletUsage } from "data/analytics/wallets/ecosystem"; +import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard"; + +export async function EcosystemAnalyticsPage({ + ecosystemId, + interval, + range, +}: { ecosystemId: string; interval: "day" | "week"; range?: Range }) { + if (!range) { + range = getLastNDaysRange("last-120"); + } + + const stats = await getEcosystemWalletUsage({ + ecosystemId, + from: range.from, + to: range.to, + period: interval, + }).catch(() => null); + + return ( +
+ + +
+ +
+ +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx new file mode 100644 index 00000000000..b8117abf309 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx @@ -0,0 +1,241 @@ +"use client"; +import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton"; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import type { EcosystemWalletStats } 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 { UnityIcon } from "components/icons/brand-icons/UnityIcon"; +import { DocLink } from "components/shared/DocLink"; +import { format } from "date-fns"; +import { formatTickerNumber } from "lib/format-utils"; +import { useMemo } from "react"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +type ChartData = Record & { + time: string; // human readable date +}; +const defaultLabel = "Unknown Auth"; + +export function EcosystemWalletUsersChartCard(props: { + ecosystemWalletStats: EcosystemWalletStats[]; + isPending: boolean; +}) { + const { ecosystemWalletStats } = props; + + const topChainsToShow = 10; + + const { chartConfig, chartData } = useMemo(() => { + const _chartConfig: ChartConfig = {}; + const _chartDataMap: Map = new Map(); + const authMethodToVolumeMap: Map = new Map(); + // for each stat, add it in _chartDataMap + for (const stat of ecosystemWalletStats) { + const chartData = _chartDataMap.get(stat.date); + const { authenticationMethod } = stat; + + // if no data for current day - create new entry + if (!chartData) { + _chartDataMap.set(stat.date, { + time: format(new Date(stat.date), "MMM dd"), + [authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected, + } as ChartData); + } else if (chartData) { + chartData[authenticationMethod || defaultLabel] = + (chartData[authenticationMethod || defaultLabel] || 0) + + stat.uniqueWalletsConnected; + } + + authMethodToVolumeMap.set( + authenticationMethod || defaultLabel, + stat.uniqueWalletsConnected + + (authMethodToVolumeMap.get(authenticationMethod || defaultLabel) || + 0), + ); + } + + const authMethodsSorted = Array.from(authMethodToVolumeMap.entries()) + .sort((a, b) => b[1] - a[1]) + .map((w) => w[0]); + + const authMethodsToShow = authMethodsSorted.slice(0, topChainsToShow); + const authMethodsAsOther = authMethodsSorted.slice(topChainsToShow); + + // replace chainIdsToTagAsOther chainId with "other" + for (const data of _chartDataMap.values()) { + for (const authMethod in data) { + if (authMethodsAsOther.includes(authMethod)) { + data.others = (data.others || 0) + (data[authMethod] || 0); + delete data[authMethod]; + } + } + } + + authMethodsToShow.forEach((walletType, i) => { + _chartConfig[walletType] = { + label: authMethodsToShow[i], + color: `hsl(var(--chart-${(i % 10) + 1}))`, + }; + }); + + if (authMethodsToShow.length > topChainsToShow) { + // Add Other + authMethodsToShow.push("others"); + _chartConfig.others = { + label: "Others", + color: "hsl(var(--muted-foreground))", + }; + } + + return { + chartData: Array.from(_chartDataMap.values()).sort( + (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(), + ), + chartConfig: _chartConfig, + }; + }, [ecosystemWalletStats]); + + const uniqueAuthMethods = Object.keys(chartConfig); + const disableActions = + props.isPending || + chartData.length === 0 || + uniqueAuthMethods.every((authMethod) => + chartData.every((data) => data[authMethod] === 0), + ); + + return ( +
+

+ Unique Users +

+

+ The total number of active users in your ecosystem for each period. +

+ +
+ { + // Shows the number of each type of wallet connected on all dates + const header = ["Date", ...uniqueAuthMethods]; + const rows = chartData.map((data) => { + const { time, ...rest } = data; + return [ + time, + ...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()), + ]; + }); + return { header, rows }; + }} + /> +
+ + {/* Chart */} + + {props.isPending ? ( + + ) : chartData.length === 0 || + uniqueAuthMethods.every((authMethod) => + chartData.every((data) => data[authMethod] === 0), + ) ? ( + +
+ + Connect users to your app with social logins + +
+ + + + +
+
+
+ ) : ( + + + + + + + Object.entries(data) + .filter(([key]) => key !== "time") + .map(([, value]) => value) + .reduce((acc, current) => Number(acc) + Number(current), 0) + } + tickLine={false} + axisLine={false} + tickFormatter={(value) => formatTickerNumber(value)} + /> + + formatTickerNumber(Number(value))} + /> + } + /> + } /> + {uniqueAuthMethods.map((authMethod) => { + return ( + + ); + })} + + )} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/Summary.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/Summary.tsx new file mode 100644 index 00000000000..1f2b99281c1 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/Summary.tsx @@ -0,0 +1,43 @@ +import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { Stat } from "components/analytics/stat"; +import { ActivityIcon, UserIcon } from "lucide-react"; + +export function EcosystemWalletsSummary(props: { + allTimeStats: EcosystemWalletStats[] | undefined; + monthlyStats: EcosystemWalletStats[] | undefined; +}) { + const allTimeStats = props.allTimeStats?.reduce( + (acc, curr) => { + acc.uniqueWalletsConnected += curr.uniqueWalletsConnected; + return acc; + }, + { + uniqueWalletsConnected: 0, + }, + ); + + const monthlyStats = props.monthlyStats?.reduce( + (acc, curr) => { + acc.uniqueWalletsConnected += curr.uniqueWalletsConnected; + return acc; + }, + { + uniqueWalletsConnected: 0, + }, + ); + + return ( +
+ + +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/page.tsx new file mode 100644 index 00000000000..133ac99e09b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/page.tsx @@ -0,0 +1,42 @@ +import type { Range } from "components/analytics/date-range-selector"; +import { fetchApiServer } from "data/analytics/fetch-api-server"; +import { FetchError } from "utils/error"; +import type { Ecosystem } from "../../../types"; +import { EcosystemAnalyticsPage } from "./components/EcosystemAnalyticsPage"; + +export default async function Page({ + params, + searchParams, +}: { + params: { slug: string }; + searchParams: { + interval?: "day" | "week"; + range?: Range; + }; +}) { + const ecosystem = await getEcosystem(params.slug); + + return ( + + ); +} + +async function getEcosystem(ecosystemSlug: string) { + const res = await fetchApiServer(`v1/ecosystem-wallet/${ecosystemSlug}`); + + if (!res.ok) { + const data = await res.json(); + console.error(data); + throw new FetchError( + res, + data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems", + ); + } + + const data = (await res.json()) as { result: Ecosystem }; + return data.result; +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemSlugLayout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx similarity index 56% rename from apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemSlugLayout.tsx rename to apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx index e771165b308..597633695db 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemSlugLayout.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx @@ -1,9 +1,10 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; +import { getEcosystemWalletUsage } from "data/analytics/wallets/ecosystem"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { getAddress } from "thirdweb"; -import { fetchEcosystem } from "../../utils/fetchEcosystem"; -import { EcosystemHeader } from "./components/client/ecosystem-header.client"; +import { fetchEcosystem } from "../../../utils/fetchEcosystem"; +import { EcosystemHeader } from "./ecosystem-header.client"; export async function EcosystemLayoutSlug({ children, @@ -30,11 +31,32 @@ export async function EcosystemLayoutSlug({ redirect(ecosystemLayoutPath); } + const allTimeStatsPromise = getEcosystemWalletUsage({ + ecosystemId: ecosystem.id, + from: new Date(2022, 0, 1), + to: new Date(), + period: "all", + }); + + const monthlyStatsPromise = getEcosystemWalletUsage({ + ecosystemId: ecosystem.id, + from: new Date(new Date().getFullYear(), new Date().getMonth(), 1), + to: new Date(), + period: "month", + }); + + const [allTimeStats, monthlyStats] = await Promise.all([ + allTimeStatsPromise, + monthlyStatsPromise, + ]); + return (
{children}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/ecosystem-header.client.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx similarity index 89% rename from apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/ecosystem-header.client.tsx rename to apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx index c3d9c306098..f08dbb3ea8f 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/ecosystem-header.client.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx @@ -15,6 +15,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { TabLinks } from "@/components/ui/tabs"; import { useThirdwebClient } from "@/constants/thirdweb.client"; import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi"; import { AlertTriangleIcon, CheckIcon, @@ -24,9 +25,10 @@ import { import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useEcosystemList } from "../../../../hooks/use-ecosystem-list"; -import type { Ecosystem } from "../../../../types"; -import { useEcosystem } from "../../hooks/use-ecosystem"; +import { useEcosystemList } from "../../../hooks/use-ecosystem-list"; +import type { Ecosystem } from "../../../types"; +import { EcosystemWalletsSummary } from "../analytics/components/Summary"; +import { useEcosystem } from "../hooks/use-ecosystem"; function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) { switch (ecosystem.status) { @@ -110,6 +112,8 @@ function EcosystemSelect(props: { export function EcosystemHeader(props: { ecosystem: Ecosystem; ecosystemLayoutPath: string; + allTimeStats: EcosystemWalletStats[]; + monthlyStats: EcosystemWalletStats[]; }) { const pathname = usePathname(); const { data: fetchedEcosystem } = useEcosystem({ @@ -193,19 +197,21 @@ export function EcosystemHeader(props: { />
+ , "mutationFn">, diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-partner.ts b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts similarity index 97% rename from apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-partner.ts rename to apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts index 74f47e80886..b7040d97220 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-partner.ts +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts @@ -4,7 +4,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; -import type { Ecosystem, Partner } from "../../../types"; +import type { Ecosystem, Partner } from "../../../../types"; type UpdatePartnerParams = { partnerId: string; diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/page.tsx new file mode 100644 index 00000000000..34eae03537d --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/page.tsx @@ -0,0 +1,5 @@ +import { EcosystemPermissionsPage } from "./components/client/EcosystemPermissionsPage"; + +export default function Page({ params }: { params: { slug: string } }) { + return ; +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/layout.tsx index 193d6af21af..488f2a0d7ac 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/layout.tsx @@ -1,4 +1,4 @@ -import { EcosystemLayoutSlug } from "./EcosystemSlugLayout"; +import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout"; export default async function Layout({ children, diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/page.tsx index c49a8d4465c..bfab2142548 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/page.tsx @@ -1,5 +1,5 @@ -import { EcosystemPermissionsPage } from "./EcosystemPermissionsPage"; +import { redirect } from "next/navigation"; export default function Page({ params }: { params: { slug: string } }) { - return ; + redirect(`/dashboard/connect/ecosystem/${params.slug}/analytics`); } diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/analytics/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/analytics/page.tsx new file mode 100644 index 00000000000..9e01c92ae4c --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/analytics/page.tsx @@ -0,0 +1,45 @@ +import type { Ecosystem } from "app/(dashboard)/dashboard/connect/ecosystem/types"; +import type { Range } from "components/analytics/date-range-selector"; +import { fetchApiServer } from "data/analytics/fetch-api-server"; +import { FetchError } from "utils/error"; +import { EcosystemAnalyticsPage } from "../../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage"; + +export default async function Page({ + params, + searchParams, +}: { + params: { + slug: string; + team_slug: string; + project_slug: string; + }; + searchParams: { + interval?: "day" | "week"; + range?: Range; + }; +}) { + const ecosystem = await getEcosystem(params.slug); + return ( + + ); +} + +async function getEcosystem(ecosystemSlug: string) { + const res = await fetchApiServer(`/v1/ecosystem-wallet/${ecosystemSlug}`); + + if (!res.ok) { + const data = await res.json(); + console.error(data); + throw new FetchError( + res, + data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems", + ); + } + + const data = (await res.json()) as { result: Ecosystem }; + return data.result; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/configuration/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/configuration/page.tsx new file mode 100644 index 00000000000..bfa744ce7be --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/configuration/page.tsx @@ -0,0 +1,5 @@ +import { EcosystemPermissionsPage } from "../../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage"; + +export default function Page({ params }: { params: { slug: string } }) { + return ; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/layout.tsx index 4909a0581e4..3949ba35a9f 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/layout.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/layout.tsx @@ -1,4 +1,4 @@ -import { EcosystemLayoutSlug } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemSlugLayout"; +import { EcosystemLayoutSlug } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/EcosystemSlugLayout"; export default async function Layout(props: { params: { team_slug: string; project_slug: string; slug: string }; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/page.tsx index c353c61fb0c..ecb3f8020d6 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/ecosystem/[slug]/(active)/page.tsx @@ -1,5 +1,9 @@ -import { EcosystemPermissionsPage } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage"; +import { redirect } from "next/navigation"; -export default function Page({ params }: { params: { slug: string } }) { - return ; +export default function Page({ + params, +}: { params: { team_slug: string; project_slug: string; slug: string } }) { + redirect( + `/team/${params.team_slug}/${params.project_slug}/connect/ecosystem/${params.slug}/analytics`, + ); } diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/RangeSelector.tsx b/apps/dashboard/src/components/analytics/range-selector.tsx similarity index 100% rename from apps/dashboard/src/components/embedded-wallets/Analytics/RangeSelector.tsx rename to apps/dashboard/src/components/analytics/range-selector.tsx diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx index 79809049f6f..97bd45c286f 100644 --- a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx @@ -26,6 +26,7 @@ import { formatTickerNumber } from "../../../lib/format-utils"; type ChartData = Record & { time: string; // human readable date }; +const defaultLabel = "Unknown Auth"; export function InAppWalletUsersChartCard(props: { inAppWalletStats: InAppWalletStats[]; @@ -47,18 +48,19 @@ export function InAppWalletUsersChartCard(props: { if (!chartData && stat.uniqueWalletsConnected > 0) { _chartDataMap.set(stat.date, { time: format(new Date(stat.date), "MMM dd"), - [authenticationMethod || "Unknown"]: stat.uniqueWalletsConnected, + [authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected, } as ChartData); } else if (chartData) { - chartData[authenticationMethod || "Unknown"] = - (chartData[authenticationMethod || "Unknown"] || 0) + + chartData[authenticationMethod || defaultLabel] = + (chartData[authenticationMethod || defaultLabel] || 0) + stat.uniqueWalletsConnected; } authMethodToVolumeMap.set( - authenticationMethod || "Unknown", + authenticationMethod || defaultLabel, stat.uniqueWalletsConnected + - (authMethodToVolumeMap.get(authenticationMethod || "Unknown") || 0), + (authMethodToVolumeMap.get(authenticationMethod || defaultLabel) || + 0), ); } @@ -86,12 +88,14 @@ export function InAppWalletUsersChartCard(props: { }; }); - // Add Other - authMethodsToShow.push("others"); - _chartConfig.others = { - label: "Others", - color: "hsl(var(--muted-foreground))", - }; + if (authMethodsToShow.length > topChainsToShow) { + // Add Other + authMethodsToShow.push("others"); + _chartConfig.others = { + label: "Others", + color: "hsl(var(--muted-foreground))", + }; + } return { chartData: Array.from(_chartDataMap.values()).sort( diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx index efa8acadaed..92baeaa62da 100644 --- a/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx +++ b/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx @@ -3,8 +3,8 @@ import { getLastNDaysRange, } from "components/analytics/date-range-selector"; import { getInAppWalletUsage } from "data/analytics/wallets/in-app"; +import { RangeSelector } from "../../analytics/range-selector"; import { InAppWalletUsersChartCard } from "./InAppWalletUsersChartCard"; -import { RangeSelector } from "./RangeSelector"; export async function InAppWalletAnalytics({ clientId, diff --git a/apps/dashboard/src/data/analytics/fetch-analytics.ts b/apps/dashboard/src/data/analytics/fetch-analytics.ts index 03f78832617..533c5ec789e 100644 --- a/apps/dashboard/src/data/analytics/fetch-analytics.ts +++ b/apps/dashboard/src/data/analytics/fetch-analytics.ts @@ -1,28 +1,31 @@ import "server-only"; -export async function fetchAnalytics(input: string | URL, init?: RequestInit) { +export async function fetchAnalytics( + input: string | URL, + init?: RequestInit, +): Promise { const [pathname, searchParams] = input.toString().split("?"); if (!pathname) { throw new Error("Invalid input, no pathname provided"); } // create a new URL object for the analytics server - const API_SERVER_URL = new URL( + const ANALYTICS_SERVICE_URL = new URL( process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com", ); - API_SERVER_URL.pathname = pathname; + ANALYTICS_SERVICE_URL.pathname = pathname; for (const param of searchParams?.split("&") || []) { const [key, value] = param.split("="); if (!key || !value) { - return; + throw new Error("Invalid input, no key or value provided"); } - API_SERVER_URL.searchParams.append( + ANALYTICS_SERVICE_URL.searchParams.append( decodeURIComponent(key), decodeURIComponent(value), ); } - return await fetch(API_SERVER_URL, { + return fetch(ANALYTICS_SERVICE_URL, { ...init, headers: { "content-type": "application/json", diff --git a/apps/dashboard/src/data/analytics/fetch-api-server.ts b/apps/dashboard/src/data/analytics/fetch-api-server.ts new file mode 100644 index 00000000000..4fa4276b00b --- /dev/null +++ b/apps/dashboard/src/data/analytics/fetch-api-server.ts @@ -0,0 +1,44 @@ +import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; +import { cookies } from "next/headers"; +import { getAddress } from "thirdweb"; +import "server-only"; + +export async function fetchApiServer( + input: string | URL, + init?: RequestInit, +): Promise { + const activeAccount = cookies().get(COOKIE_ACTIVE_ACCOUNT)?.value; + const authToken = activeAccount + ? cookies().get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value + : null; + + const [pathname, searchParams] = input.toString().split("?"); + if (!pathname) { + throw new Error("Invalid input, no pathname provided"); + } + + // create a new URL object for the analytics server + const API_SERVER_URL = new URL( + process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com", + ); + API_SERVER_URL.pathname = pathname; + for (const param of searchParams?.split("&") || []) { + const [key, value] = param.split("="); + if (!key || !value) { + throw new Error("Invalid input, no key or value provided"); + } + API_SERVER_URL.searchParams.append( + decodeURIComponent(key), + decodeURIComponent(value), + ); + } + + return fetch(API_SERVER_URL, { + ...init, + headers: { + "content-type": "application/json", + ...(authToken ? { authorization: `Bearer ${authToken}` } : {}), + ...init?.headers, + }, + }); +} diff --git a/apps/dashboard/src/data/analytics/wallets/ecosystem.ts b/apps/dashboard/src/data/analytics/wallets/ecosystem.ts new file mode 100644 index 00000000000..561f5ce55e2 --- /dev/null +++ b/apps/dashboard/src/data/analytics/wallets/ecosystem.ts @@ -0,0 +1,40 @@ +import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi"; +import { fetchAnalytics } from "../fetch-analytics"; + +export async function getEcosystemWalletUsage(args: { + ecosystemId: string; + from?: Date; + to?: Date; + period?: "day" | "week" | "month" | "year" | "all"; +}) { + const { ecosystemId, from, to, period } = args; + + const searchParams = new URLSearchParams(); + if (from) { + searchParams.append("from", from.toISOString()); + } + if (to) { + searchParams.append("to", to.toISOString()); + } + if (period) { + searchParams.append("period", period); + } + const res = await fetchAnalytics( + `v1/wallets/ecosystem/${ecosystemId}?${searchParams.toString()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (res?.status !== 200) { + console.error("Failed to fetch in-app wallet stats"); + return null; + } + + const json = await res.json(); + + return json.data as EcosystemWalletStats[]; +}