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[];
+}