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 -

- - -
- - {/* 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; +}