diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
index 06376c7e75b..b6461cf3cca 100644
--- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
+++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts
@@ -235,6 +235,12 @@ export interface WalletStats {
walletType: string;
}
+export interface InAppWalletStats {
+ date: string;
+ authenticationMethod: string;
+ uniqueWalletsConnected: number;
+}
+
export interface UserOpStats {
date: string;
successful: number;
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_components/tabs.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_components/tabs.tsx
new file mode 100644
index 00000000000..234bf1fe346
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_components/tabs.tsx
@@ -0,0 +1,32 @@
+"use client";
+import { TabLinks } from "@/components/ui/tabs";
+import { usePathname } from "next/navigation";
+
+export function Tabs({ clientId }: { clientId: string }) {
+ const path = usePathname();
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_constants.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_constants.tsx
new file mode 100644
index 00000000000..37103397a9c
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/_constants.tsx
@@ -0,0 +1 @@
+export const TRACKING_CATEGORY = "embedded-wallet";
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/analytics/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/analytics/page.tsx
new file mode 100644
index 00000000000..cca2aa5301d
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/analytics/page.tsx
@@ -0,0 +1,37 @@
+import type { Range } from "components/analytics/date-range-selector";
+import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
+
+export default function Page({
+ params,
+ searchParams,
+}: {
+ params: { team_slug: string; project_slug: string };
+ searchParams: {
+ from?: string;
+ to?: string;
+ type?: string;
+ interval?: string;
+ };
+}) {
+ const range =
+ searchParams.from && searchParams.to
+ ? {
+ type: searchParams.type ?? "last-120",
+ from: new Date(searchParams.from),
+ to: new Date(searchParams.to),
+ }
+ : undefined;
+
+ const interval: "day" | "week" = ["day", "week"].includes(
+ searchParams.interval ?? "",
+ )
+ ? (searchParams.interval as "day" | "week")
+ : "week";
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/config/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/config/page.tsx
new file mode 100644
index 00000000000..46637440e6d
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/config/page.tsx
@@ -0,0 +1,21 @@
+import { InAppWalletSettingsPage } from "components/embedded-wallets/Configure";
+import { redirect } from "next/navigation";
+import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
+import { TRACKING_CATEGORY } from "../_constants";
+
+export default async function Page({
+ params: { clientId },
+}: { params: { clientId: string } }) {
+ const apiKeys = await getInAppWalletSupportedAPIKeys();
+ const apiKey = apiKeys.find((key) => key.key === clientId);
+
+ if (!apiKey) {
+ redirect("/dashboard/connect/in-app-wallets");
+ }
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/layout.tsx
new file mode 100644
index 00000000000..272d12ba9f4
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/layout.tsx
@@ -0,0 +1,78 @@
+import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
+import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
+import { redirect } from "next/navigation";
+import { getAuthToken } from "../../../../../api/lib/getAuthToken";
+import { PageHeader } from "../PageHeader";
+import { getInAppWalletSupportedAPIKeys } from "../getInAppWalletSupportedAPIKeys";
+import { InAppWalletsAPIKeysMenu } from "../inAppWalletsAPIKeysMenu";
+import { Tabs } from "./_components/tabs";
+
+export default async function Page(props: {
+ params: {
+ clientId: string;
+ };
+ searchParams: {
+ tab?: string;
+ };
+ children: React.ReactNode;
+}) {
+ const authToken = getAuthToken();
+ const { clientId } = props.params;
+
+ if (!authToken) {
+ redirect(
+ `/login?next=${encodeURIComponent(`/dashboard/connect/in-app-wallets/${clientId}`)}`,
+ );
+ }
+
+ const apiKeys = await getInAppWalletSupportedAPIKeys();
+ const apiKey = apiKeys.find((key) => key.key === clientId);
+
+ if (!apiKey) {
+ redirect("/dashboard/connect/in-app-wallets");
+ }
+
+ const allTimeStats = await getInAppWalletUsage({
+ clientId,
+ from: new Date(2022, 0, 1),
+ to: new Date(),
+ period: "all",
+ });
+
+ const monthlyStats = await getInAppWalletUsage({
+ clientId,
+ from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
+ to: new Date(),
+ period: "month",
+ });
+
+ return (
+
+ {/* header */}
+
+
+
+ ({
+ name: x.name,
+ key: x.key,
+ }))}
+ selectedAPIKey={apiKey}
+ />
+
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/page.tsx
index 8e0bb1cacb1..c6bf1a1a74a 100644
--- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/page.tsx
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/page.tsx
@@ -1,9 +1,4 @@
import { redirect } from "next/navigation";
-import { EmbeddedWallets } from "../../../../../../components/embedded-wallets";
-import { getAuthToken } from "../../../../../api/lib/getAuthToken";
-import { PageHeader } from "../PageHeader";
-import { getInAppWalletSupportedAPIKeys } from "../getInAppWalletSupportedAPIKeys";
-import { InAppWalletsAPIKeysMenu } from "../inAppWalletsAPIKeysMenu";
export default async function Page(props: {
params: {
@@ -13,44 +8,7 @@ export default async function Page(props: {
tab?: string;
};
}) {
- const authToken = getAuthToken();
const { clientId } = props.params;
- if (!authToken) {
- redirect(
- `/login?next=${encodeURIComponent(`/dashboard/connect/in-app-wallets/${clientId}`)}`,
- );
- }
-
- const apiKeys = await getInAppWalletSupportedAPIKeys();
- const apiKey = apiKeys.find((key) => key.key === clientId);
-
- if (!apiKey) {
- redirect("/dashboard/connect/in-app-wallets");
- }
-
- return (
-
- {/* header */}
-
-
-
- ({
- name: x.name,
- key: x.key,
- }))}
- selectedAPIKey={apiKey}
- />
-
-
-
-
-
-
- );
+ redirect(`/dashboard/connect/in-app-wallets/${clientId}/analytics`);
}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/users/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/users/page.tsx
new file mode 100644
index 00000000000..d174bb901fe
--- /dev/null
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/[clientId]/users/page.tsx
@@ -0,0 +1,24 @@
+import { InAppWalletUsersPageContent } from "components/embedded-wallets/Users";
+import { redirect } from "next/navigation";
+import { getInAppWalletSupportedAPIKeys } from "../../getInAppWalletSupportedAPIKeys";
+import { TRACKING_CATEGORY } from "../_constants";
+
+export default async function Page(props: {
+ params: {
+ clientId: string;
+ };
+}) {
+ const { clientId } = props.params;
+ const apiKeys = await getInAppWalletSupportedAPIKeys();
+ const apiKey = apiKeys.find((key) => key.key === clientId);
+
+ if (!apiKey) {
+ redirect("/dashboard/connect/in-app-wallets");
+ }
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/layout.tsx
index 2c9758c30e2..2c760b46800 100644
--- a/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/layout.tsx
+++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/in-app-wallets/layout.tsx
@@ -1,4 +1,3 @@
-import { AnalyticsCallout } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/AnalyticsCallout";
import { InAppWaletFooterSection } from "../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
export default function Layout(props: {
@@ -9,7 +8,6 @@ export default function Layout(props: {
{props.children}
{/* Footer */}
-
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/AnalyticsCallout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/AnalyticsCallout.tsx
deleted file mode 100644
index 2022c9f0c51..00000000000
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/AnalyticsCallout.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { TrackedLinkTW } from "@/components/ui/tracked-link";
-import { useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi";
-import { UsageCard } from "components/settings/Account/UsageCard";
-import { ArrowRightIcon } from "lucide-react";
-import { useMemo } from "react";
-import { toNumber, toPercent } from "utils/number";
-
-type AnalyticsCalloutProps = {
- trackingCategory: string;
-};
-
-export const AnalyticsCallout: React.FC = ({
- trackingCategory,
-}) => {
- const usageQuery = useAccountUsage();
-
- const walletsMetrics = useMemo(() => {
- if (!usageQuery?.data) {
- return undefined;
- }
-
- const usageData = usageQuery.data;
-
- const numOfWallets = usageData.usage.embeddedWallets.countWalletAddresses;
- const limitWallets = usageData.limits.embeddedWallets;
- const percent = toPercent(numOfWallets, limitWallets);
-
- return {
- total: `${toNumber(numOfWallets)} / ${toNumber(
- limitWallets,
- )} (${percent}%)`,
- progress: percent,
- ...(usageData.billableUsd.embeddedWallets > 0
- ? {
- overage: usageData.billableUsd.embeddedWallets,
- }
- : {}),
- };
- }, [usageQuery]);
-
- return (
-
- {/* Left */}
-
-
Analytics
-
- View more insights about how users are interacting with your
- application
-
-
-
-
- View Analytics
-
-
-
-
-
- {/* Right */}
-
- {walletsMetrics && (
-
- )}
-
-
- );
-};
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/header.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/header.tsx
new file mode 100644
index 00000000000..e3887007cdb
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/header.tsx
@@ -0,0 +1,46 @@
+import { TrackedLinkTW } from "@/components/ui/tracked-link";
+import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
+import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
+import { TRACKING_CATEGORY } from "../_constants";
+
+export async function InAppWalletsHeader({ clientId }: { clientId: string }) {
+ const allTimeStats = await getInAppWalletUsage({
+ clientId,
+ from: new Date(2022, 0, 1),
+ to: new Date(),
+ period: "all",
+ });
+
+ const monthlyStats = await getInAppWalletUsage({
+ clientId,
+ from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
+ to: new Date(),
+ period: "month",
+ });
+
+ return (
+
+
+ In-App Wallets
+
+
+ A wallet infrastructure that enables apps to create, manage, and control
+ their users wallets. Email login, social login, and bring-your-own auth
+ supported.{" "}
+
+ Learn more
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/tabs.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/tabs.tsx
new file mode 100644
index 00000000000..07de192b944
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/tabs.tsx
@@ -0,0 +1,35 @@
+"use client";
+import { TabLinks } from "@/components/ui/tabs";
+import { usePathname } from "next/navigation";
+
+export function Tabs({
+ team_slug,
+ project_slug,
+}: { team_slug: string; project_slug: string }) {
+ const path = usePathname();
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_constants.ts b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_constants.ts
new file mode 100644
index 00000000000..f50b20dd636
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/_constants.ts
@@ -0,0 +1 @@
+export const TRACKING_CATEGORY = "team/in-app-wallets";
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/analytics/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/analytics/page.tsx
new file mode 100644
index 00000000000..a97022b14fd
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/analytics/page.tsx
@@ -0,0 +1,38 @@
+import type { Range } from "components/analytics/date-range-selector";
+import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
+
+export default function Page({
+ params,
+ searchParams,
+}: {
+ params: { team_slug: string; project_slug: string };
+ searchParams: {
+ from?: string;
+ to?: string;
+ type?: string;
+ interval?: string;
+ };
+}) {
+ const range =
+ searchParams.from && searchParams.to
+ ? {
+ type: searchParams.type ?? "last-120",
+ from: new Date(searchParams.from),
+ to: new Date(searchParams.to),
+ }
+ : undefined;
+
+ const interval: "day" | "week" = ["day", "week"].includes(
+ searchParams.interval ?? "",
+ )
+ ? (searchParams.interval as "day" | "week")
+ : "week";
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx
new file mode 100644
index 00000000000..5b273c4fde9
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx
@@ -0,0 +1,30 @@
+import { getProject } from "@/api/projects";
+import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
+import { notFound } from "next/navigation";
+import { InAppWalletSettingsPage } from "../../../../../../../components/embedded-wallets/Configure";
+import { TRACKING_CATEGORY } from "../_constants";
+
+export default async function Page({
+ params,
+}: { params: { team_slug: string; project_slug: string } }) {
+ const project = await getProject(params.team_slug, params.project_slug);
+
+ if (!project) {
+ notFound();
+ }
+
+ const apiKey = await getAPIKeyForProjectId(project.id);
+
+ if (!apiKey) {
+ notFound();
+ }
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
new file mode 100644
index 00000000000..ff42ad03171
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
@@ -0,0 +1,49 @@
+import { getProject } from "@/api/projects";
+import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
+import { notFound } from "next/navigation";
+import { InAppWaletFooterSection } from "./_components/footer";
+import { InAppWalletsHeader } from "./_components/header";
+import { Tabs } from "./_components/tabs";
+import { TRACKING_CATEGORY } from "./_constants";
+
+export default async function Layout(props: {
+ params: {
+ team_slug: string;
+ project_slug: string;
+ };
+ children: React.ReactNode;
+}) {
+ const project = await getProject(
+ props.params.team_slug,
+ props.params.project_slug,
+ );
+ if (!project) {
+ notFound();
+ }
+
+ const apiKey = await getAPIKeyForProjectId(project.id);
+ if (!apiKey) {
+ notFound();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/page.tsx
index 1b79776584b..811e88fcf48 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/page.tsx
@@ -1,9 +1,4 @@
-import { getProject } from "@/api/projects";
-import { TrackedLinkTW } from "@/components/ui/tracked-link";
-import { notFound } from "next/navigation";
-import { InAppWalletUsersPageContent } from "../../../../../../components/embedded-wallets/Users";
-import { AnalyticsCallout } from "./_components/AnalyticsCallout";
-import { InAppWaletFooterSection } from "./_components/footer";
+import { redirect } from "next/navigation";
export default async function Page(props: {
params: {
@@ -11,48 +6,8 @@ export default async function Page(props: {
project_slug: string;
};
}) {
- const project = await getProject(
- props.params.team_slug,
- props.params.project_slug,
- );
-
- if (!project) {
- notFound();
- }
-
- const TRACKING_CATEGORY = "team/in-app-wallets";
-
- return (
-
-
- In-App Wallets
-
-
-
- A wallet infrastructure that enables apps to create, manage, and control
- their users wallets. Email login, social login, and bring-your-own auth
- supported.{" "}
-
- Learn more
-
-
-
-
-
-
-
-
-
-
-
+ // Default to the users tab
+ redirect(
+ `/team/${props.params.team_slug}/${props.params.project_slug}/connect/in-app-wallets/analytics`,
);
}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx
new file mode 100644
index 00000000000..8b3683ae657
--- /dev/null
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/users/page.tsx
@@ -0,0 +1,15 @@
+import { InAppWalletUsersPageContent } from "components/embedded-wallets/Users";
+import { TRACKING_CATEGORY } from "../_constants";
+
+export default function Page({
+ params,
+}: { params: { team_slug: string; project_slug: string } }) {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx
new file mode 100644
index 00000000000..67ec0be8bea
--- /dev/null
+++ b/apps/dashboard/src/components/embedded-wallets/Analytics/InAppWalletUsersChartCard.tsx
@@ -0,0 +1,216 @@
+"use client";
+import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
+import {
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import type { InAppWalletStats } 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 { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
+import { DocLink } from "components/shared/DocLink";
+import { format } from "date-fns";
+import { useMemo } from "react";
+import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
+
+type ChartData = Record & {
+ time: string; // human readable date
+};
+
+export function InAppWalletUsersChartCard(props: {
+ inAppWalletStats: InAppWalletStats[];
+ isPending: boolean;
+}) {
+ const { inAppWalletStats } = 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 inAppWalletStats) {
+ const chartData = _chartDataMap.get(stat.date);
+ const { authenticationMethod } = stat;
+
+ // if no data for current day - create new entry
+ if (!chartData && stat.uniqueWalletsConnected > 0) {
+ _chartDataMap.set(stat.date, {
+ time: format(new Date(stat.date), "MMM dd"),
+ [authenticationMethod || "Unknown"]: stat.uniqueWalletsConnected,
+ } as ChartData);
+ } else if (chartData) {
+ chartData[authenticationMethod || "Unknown"] =
+ (chartData[authenticationMethod || "Unknown"] || 0) +
+ stat.uniqueWalletsConnected;
+ }
+
+ authMethodToVolumeMap.set(
+ authenticationMethod || "Unknown",
+ stat.uniqueWalletsConnected +
+ (authMethodToVolumeMap.get(authenticationMethod || "Unknown") || 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}))`,
+ };
+ });
+
+ // Add Other
+ authMethodsToShow.push("others");
+ _chartConfig.others = {
+ label: "Others",
+ color: "hsl(var(--muted-foreground))",
+ };
+
+ return {
+ chartData: Array.from(_chartDataMap.values()),
+ chartConfig: _chartConfig,
+ };
+ }, [inAppWalletStats]);
+
+ const uniqueAuthMethods = Object.keys(chartConfig);
+ const disableActions =
+ props.isPending ||
+ chartData.length === 0 ||
+ chartData.every((data) => data.sponsoredUsd === 0);
+
+ return (
+
+
+ Unique Users
+
+
+ The total number of active in-app wallet users on your project.
+
+
+
+ {
+ // 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 ||
+ chartData.every((data) => data.sponsoredUsd === 0) ? (
+
+
+
+ Connect users to your app with social logins
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ } />
+ } />
+ {uniqueAuthMethods.map((authMethod) => {
+ return (
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/RangeSelector.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/RangeSelector.tsx
new file mode 100644
index 00000000000..d929a5939fc
--- /dev/null
+++ b/apps/dashboard/src/components/embedded-wallets/Analytics/RangeSelector.tsx
@@ -0,0 +1,42 @@
+"use client";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { DateRangeSelector } from "components/analytics/date-range-selector";
+import type { Range } from "components/analytics/date-range-selector";
+import { IntervalSelector } from "components/analytics/interval-selector";
+import { differenceInDays } from "date-fns";
+import { usePathname, useSearchParams } from "next/navigation";
+
+export function RangeSelector({
+ range,
+ interval,
+}: { range: Range; interval: "day" | "week" }) {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const router = useDashboardRouter();
+
+ return (
+
+ {
+ const days = differenceInDays(newRange.to, newRange.from);
+ const interval = days > 30 ? "week" : "day";
+ const newSearchParams = new URLSearchParams(searchParams || {});
+ newSearchParams.set("from", newRange.from.toISOString());
+ newSearchParams.set("to", newRange.to.toISOString());
+ newSearchParams.set("type", newRange.type);
+ newSearchParams.set("interval", interval);
+ router.push(`${pathname}?${newSearchParams.toString()}`);
+ }}
+ />
+ {
+ const newSearchParams = new URLSearchParams(searchParams || {});
+ newSearchParams.set("interval", newInterval);
+ router.push(`${pathname}?${newSearchParams.toString()}`);
+ }}
+ />
+
+ );
+}
diff --git a/apps/dashboard/src/components/embedded-wallets/Analytics/Summary.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/Summary.tsx
new file mode 100644
index 00000000000..d3438d293ff
--- /dev/null
+++ b/apps/dashboard/src/components/embedded-wallets/Analytics/Summary.tsx
@@ -0,0 +1,43 @@
+import type { InAppWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
+import { Stat } from "components/analytics/stat";
+import { ActivityIcon, UserIcon } from "lucide-react";
+
+export function InAppWalletsSummary(props: {
+ allTimeStats: InAppWalletStats[];
+ monthlyStats: InAppWalletStats[];
+}) {
+ 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/components/embedded-wallets/Analytics/index.tsx b/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx
new file mode 100644
index 00000000000..6203c9d62b0
--- /dev/null
+++ b/apps/dashboard/src/components/embedded-wallets/Analytics/index.tsx
@@ -0,0 +1,36 @@
+import {
+ type Range,
+ getLastNDaysRange,
+} from "components/analytics/date-range-selector";
+import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
+import { InAppWalletUsersChartCard } from "./InAppWalletUsersChartCard";
+import { RangeSelector } from "./RangeSelector";
+
+export async function InAppWalletAnalytics({
+ clientId,
+ interval,
+ range,
+}: { clientId: string; interval: "day" | "week"; range?: Range }) {
+ if (!range) {
+ range = getLastNDaysRange("last-120");
+ }
+
+ const stats = await getInAppWalletUsage({
+ clientId,
+ from: range.from,
+ to: range.to,
+ period: interval,
+ });
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx b/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
index 3873d8106a3..3ba63154756 100644
--- a/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
+++ b/apps/dashboard/src/components/embedded-wallets/Configure/index.tsx
@@ -14,7 +14,6 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
@@ -230,7 +229,7 @@ export const InAppWalletSettingsUI: React.FC<
canEditAdvancedFeatures={canEditAdvancedFeatures}
/>
-
+
;
- trackingCategory: string;
- defaultTab: 0 | 1;
-}
-
-export const EmbeddedWallets: React.FC = ({
- apiKey,
- trackingCategory,
- defaultTab,
-}) => {
- const [selectedTab, setSelectedTab] = useState<"users" | "config">(
- defaultTab === 0 ? "users" : "config",
- );
-
- function updateSearchParams(value: string) {
- const url = new URL(window.location.href);
- url.searchParams.set("tab", value);
- window.history.pushState(null, "", url.toString());
- }
-
- return (
-
-
{
- setSelectedTab("users");
- updateSearchParams("0");
- },
- isActive: selectedTab === "users",
- isEnabled: true,
- },
- {
- name: "Configuration",
- onClick: () => {
- setSelectedTab("config");
- updateSearchParams("1");
- },
- isActive: selectedTab === "config",
- isEnabled: true,
- },
- ]}
- />
-
-
-
- {selectedTab === "users" && (
-
- )}
-
- {selectedTab === "config" && (
-
- )}
-
- );
-};
diff --git a/apps/dashboard/src/data/analytics/fetch-analytics.ts b/apps/dashboard/src/data/analytics/fetch-analytics.ts
new file mode 100644
index 00000000000..03f78832617
--- /dev/null
+++ b/apps/dashboard/src/data/analytics/fetch-analytics.ts
@@ -0,0 +1,33 @@
+import "server-only";
+
+export async function fetchAnalytics(input: string | URL, init?: RequestInit) {
+ 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.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
+ );
+ API_SERVER_URL.pathname = pathname;
+ for (const param of searchParams?.split("&") || []) {
+ const [key, value] = param.split("=");
+ if (!key || !value) {
+ return;
+ }
+ API_SERVER_URL.searchParams.append(
+ decodeURIComponent(key),
+ decodeURIComponent(value),
+ );
+ }
+
+ return await fetch(API_SERVER_URL, {
+ ...init,
+ headers: {
+ "content-type": "application/json",
+ authorization: `Bearer ${process.env.ANALYTICS_SERVICE_API_KEY}`,
+ ...init?.headers,
+ },
+ });
+}
diff --git a/apps/dashboard/src/data/analytics/wallets/in-app.ts b/apps/dashboard/src/data/analytics/wallets/in-app.ts
new file mode 100644
index 00000000000..0ebfb54585f
--- /dev/null
+++ b/apps/dashboard/src/data/analytics/wallets/in-app.ts
@@ -0,0 +1,38 @@
+import { fetchAnalytics } from "../fetch-analytics";
+
+export async function getInAppWalletUsage(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 fetchAnalytics(
+ `v1/wallets/in-app?${searchParams.toString()}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ },
+ );
+ const json = await res?.json();
+
+ if (!res || res.status !== 200) {
+ throw new Error(json.message);
+ }
+
+ return json.data;
+}