diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx index 0ea10a6deb9..1f1ae5e15b8 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx @@ -1,54 +1,172 @@ +import { notFound, redirect } from "next/navigation"; + +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import { fetchAnalytics } from "data/analytics/fetch-analytics"; + +import type { + InAppWalletStats, + UserOpStatsByChain, + WalletStats, + WalletUserStats, +} from "@3rdweb-sdk/react/hooks/useApi"; +import { + type DurationId, + type Range, + getLastNDaysRange, +} from "components/analytics/date-range-selector"; + +import { + type ChainMetadata, + defineChain, + getChainMetadata, +} from "thirdweb/chains"; +import { type WalletId, getWalletInfo } from "thirdweb/wallets"; + +import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; +import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader"; +import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard"; +import { EmptyState } from "../../components/Analytics/EmptyState"; +import { PieChartCard } from "../../components/Analytics/PieChartCard"; +import { StatBreakdownCard } from "../../components/Analytics/StatBreakdownCard"; + +import { getTeamBySlug } from "@/api/team"; +import { getAccount } from "app/account/settings/getAccount"; import { Changelog, type ChangelogItem } from "components/dashboard/Changelog"; -import { HomeProductCard } from "components/dashboard/HomeProductCard"; -import { OnboardingSteps } from "components/onboarding/Steps"; -import { PRODUCTS } from "components/product-pages/common/nav/data"; -import { BillingAlerts } from "components/settings/Account/Billing/alerts/Alert"; -const TRACKING_CATEGORY = "dashboard"; +// revalidate every 5 minutes +export const revalidate = 300; -export default async function Page() { +export default async function Page(props: { + params: { team_slug: string }; + searchParams: { + usersChart?: string; + from?: string; + to?: string; + type?: string; + interval?: string; + }; +}) { const changelog = await getChangelog(); + const [params, searchParams] = await Promise.all([ + props.params, + props.searchParams, + ]); + + const team = await getTeamBySlug(params.team_slug); + const account = await getAccount(); + const interval = (searchParams.interval as "day" | "week") ?? "week"; + const rangeType = (searchParams.type as DurationId) || "last-120"; + const range: Range = { + from: new Date(searchParams.from ?? getLastNDaysRange("last-120").from), + to: new Date(searchParams.to ?? getLastNDaysRange("last-120").to), + type: rangeType, + }; + + if (!team) { + notFound(); + } + + if (!account) { + redirect("/login"); + } + + const [ + walletConnections, + walletUserStatsTimeSeries, + inAppWalletUsage, + userOpUsage, + ] = await Promise.all([ + // Aggregated wallet connections + getWalletConnections({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + // Time series data for wallet users + getWalletUsers({ + accountId: account.id, + from: range.from, + to: range.to, + period: interval, + }), + // In-app wallet usage + getInAppWalletUsage({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + // User operations usage + getUserOpUsage({ + accountId: account.id, + from: range.from, + to: range.to, + period: "all", + }), + ]); + + const isEmpty = + !walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) && + walletConnections.length === 0 && + inAppWalletUsage.length === 0 && + userOpUsage.length === 0; return ( -
-
-

- Get started quickly -

-
- - -
- {["connect", "contracts", "infrastructure"].map((section) => { - const products = PRODUCTS.filter( - (p) => p.section === section && !!p.dashboardLink, - ); - - return ( -
-

- {section === "infrastructure" ? "Engine" : section} -

-
- {products.map((product) => ( - - ))} -
+
+
+ +
+
+
+ {isEmpty ? ( +
+ +
+ ) : ( +
+ {walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) && ( +
+
- ); - })} -
+ )} +
+ {walletConnections.length > 0 && ( + + )} + {inAppWalletUsage.length > 0 && ( + + )} +
+ {userOpUsage.length > 0 && ( +
+ + +
+ )} +
+ )} +
+
+

+ Latest changes +

+
-
-
-

- Latest changes -

-
); @@ -62,5 +180,349 @@ async function getChangelog() { return json.posts as ChangelogItem[]; } -// revalidate every 5 minutes -export const revalidate = 300; +type UserMetrics = { + totalUsers: number; + activeUsers: number; + newUsers: number; + returningUsers: number; +}; + +type TimeSeriesMetrics = UserMetrics & { + date: string; +}; + +function processTimeSeriesData( + userStats: WalletUserStats[], +): TimeSeriesMetrics[] { + const metrics: TimeSeriesMetrics[] = []; + + let cumulativeUsers = 0; + for (const stat of userStats) { + cumulativeUsers += stat.newUsers ?? 0; + metrics.push({ + date: stat.date, + activeUsers: stat.totalUsers ?? 0, + returningUsers: stat.returningUsers ?? 0, + newUsers: stat.newUsers ?? 0, + totalUsers: cumulativeUsers, + }); + } + + return metrics; +} + +function UsersChartCard({ + chartKey, + userStats, + searchParams, +}: { + chartKey: keyof UserMetrics; + userStats: WalletUserStats[]; + searchParams?: { [key: string]: string | string[] | undefined }; +}) { + const timeSeriesData = processTimeSeriesData(userStats); + + const chartConfig = { + activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" }, + totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" }, + newUsers: { label: "New Users", color: "hsl(var(--chart-3))" }, + returningUsers: { + label: "Returning Users", + color: "hsl(var(--chart-4))", + }, + } as const; + + return ( + + timeSeriesData[timeSeriesData.length - 1]?.[key] + } + // Get the trend from the last two COMPLETE periods + trendFn={(data, key) => + data.filter((d) => (d[key] as number) > 0).length >= 3 + ? ((data[data.length - 2]?.[key] as number) ?? 0) / + ((data[data.length - 3]?.[key] as number) ?? 0) - + 1 + : undefined + } + existingQueryParams={searchParams} + /> + ); +} + +async function WalletDistributionCard({ data }: { data: WalletStats[] }) { + const formattedData = await Promise.all( + data + .filter((w) => w.walletType !== "smart" && w.walletType !== "smartWallet") + .map(async (w) => { + const wallet = await getWalletInfo(w.walletType as WalletId).catch( + () => ({ name: w.walletType }), + ); + return { + walletType: w.walletType, + uniqueWalletsConnected: w.uniqueWalletsConnected, + totalConnections: w.totalConnections, + walletName: wallet.name, + }; + }), + ); + + return ( + { + return { + value: uniqueWalletsConnected, + label: walletName, + }; + })} + /> + ); +} + +function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) { + return ( + ({ + value: uniqueWalletsConnected, + label: authenticationMethod, + }))} + /> + ); +} + +async function TotalSponsoredCard({ data }: { data: UserOpStatsByChain[] }) { + const chains = await Promise.all( + data.map( + (item) => + // eslint-disable-next-line no-restricted-syntax + item.chainId && getChainMetadata(defineChain(Number(item.chainId))), + ), + ).then((chains) => chains.filter((c) => c) as ChainMetadata[]); + + return ( + b.sponsoredUsd - a.sponsoredUsd) + .map((item, index) => { + const chain = chains.find((c) => c.chainId === Number(item.chainId)); + return { + label: chain?.name || item.chainId || "Unknown", + value: item.sponsoredUsd, + icon: chain?.icon?.url ? ( +
+ +
+ ) : undefined, + fill: `hsl(var(--chart-${index + 1}))`, + }; + })} + /> + ); +} + +async function UserOpUsageCard({ data }: { data: UserOpStatsByChain[] }) { + const chains = await Promise.all( + data.map( + (item) => + // eslint-disable-next-line no-restricted-syntax + item.chainId && getChainMetadata(defineChain(Number(item.chainId))), + ), + ).then((chains) => chains.filter((c) => c) as ChainMetadata[]); + + return ( + b.successful - a.successful) + .map((item, index) => { + const chain = chains.find((c) => c.chainId === Number(item.chainId)); + + return { + label: chain?.name || item.chainId || "Unknown", + value: item.successful + item.failed, + icon: chain?.icon?.url ? ( +
+ +
+ ) : undefined, + fill: `hsl(var(--chart-${index + 1}))`, + }; + })} + /> + ); +} + +export async function getWalletConnections(args: { + accountId: string; + from?: Date; + to?: Date; + period?: "day" | "week" | "month" | "year" | "all"; +}): Promise { + const { accountId, from, to, period } = args; + + const searchParams = new URLSearchParams(); + searchParams.append("accountId", accountId); + 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?${searchParams.toString()}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (res?.status !== 200) { + console.error("Failed to fetch wallet connections"); + return []; + } + + const json = await res.json(); + + return json.data as WalletStats[]; +} + +export async function getInAppWalletUsage(args: { + accountId: string; + from?: Date; + to?: Date; + period?: "day" | "week" | "month" | "year" | "all"; +}): Promise { + const { accountId, from, to, period } = args; + + const searchParams = new URLSearchParams(); + searchParams.append("accountId", accountId); + 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", + }, + }, + ); + + if (res?.status !== 200) { + console.error("Failed to fetch in-app wallet usage"); + return []; + } + + const json = await res.json(); + + return json.data as InAppWalletStats[]; +} + +async function getUserOpUsage(args: { + accountId: string; + from?: Date; + to?: Date; + period?: "day" | "week" | "month" | "year" | "all"; +}) { + const { accountId, from, to, period } = args; + + const searchParams = new URLSearchParams(); + searchParams.append("accountId", accountId); + 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/user-ops?${searchParams.toString()}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const json = await res.json(); + + if (res.status !== 200) { + console.error("Failed to fetch user ops usage"); + return []; + } + + return json.data as UserOpStatsByChain[]; +} + +async function getWalletUsers(args: { + accountId: string; + from?: Date; + to?: Date; + period?: "day" | "week" | "month" | "year" | "all"; +}): Promise { + const { accountId, from, to, period } = args; + + const searchParams = new URLSearchParams(); + searchParams.append("accountId", accountId); + 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/users?${searchParams.toString()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (res?.status !== 200) { + console.error("Failed to fetch wallet user stats"); + return []; + } + + const json = await res.json(); + + return json.data as WalletUserStats[]; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx index e25257be171..0003d6cbdf4 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx @@ -24,11 +24,11 @@ import { import { type WalletId, getWalletInfo } from "thirdweb/wallets"; import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler"; -import { CombinedBarChartCard } from "./components/CombinedBarChartCard"; -import { EmptyState } from "./components/EmptyState"; -import { PieChartCard } from "./components/PieChartCard"; -import { ProjectOverviewHeader } from "./components/ProjectOverviewHeader"; -import { StatBreakdownCard } from "./components/StatBreakdownCard"; +import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader"; +import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard"; +import { EmptyState } from "../../components/Analytics/EmptyState"; +import { PieChartCard } from "../../components/Analytics/PieChartCard"; +import { StatBreakdownCard } from "../../components/Analytics/StatBreakdownCard"; type PageParams = { team_slug: string; @@ -111,10 +111,10 @@ export default async function ProjectOverviewPage(props: PageProps) { userOpUsage.length === 0; return ( -
+
- diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.stories.tsx similarity index 79% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.stories.tsx index f87fc30cec6..a7043b6df38 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.stories.tsx +++ b/apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.stories.tsx @@ -1,11 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; import { getLastNDaysRange } from "components/analytics/date-range-selector"; -import { projectStub } from "stories/stubs"; import { BadgeContainer, mobileViewport } from "stories/utils"; -import { ProjectOverviewHeader } from "./ProjectOverviewHeader"; +import { AnalyticsHeader } from "./AnalyticsHeader"; const meta = { - title: "project/Overview/Header", + title: "Analytics/AnalyticsHeader", component: Component, parameters: { layout: "centered", @@ -36,8 +35,8 @@ function Component() { return (
- diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.tsx b/apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.tsx similarity index 63% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.tsx rename to apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.tsx index 92dec5cd35a..57ab6f32ed9 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/ProjectOverviewHeader.tsx +++ b/apps/dashboard/src/app/team/components/Analytics/AnalyticsHeader.tsx @@ -1,20 +1,17 @@ -import type { Project } from "@/api/projects"; import type { Range } from "components/analytics/date-range-selector"; import { RangeSelector } from "components/analytics/range-selector"; -export function ProjectOverviewHeader(props: { - project: Project; +export function AnalyticsHeader(props: { + title: string; interval: "day" | "week"; range: Range; }) { - const { project, interval, range } = props; + const { title, interval, range } = props; return (
-

- {project.name} -

+

{title}

diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/BarChart.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/BarChart.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx b/apps/dashboard/src/app/team/components/Analytics/BarChart.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/BarChart.tsx rename to apps/dashboard/src/app/team/components/Analytics/BarChart.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx b/apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/CombinedBarChartCard.tsx rename to apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/EmptyState.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/EmptyState.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.tsx b/apps/dashboard/src/app/team/components/Analytics/EmptyState.tsx similarity index 87% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.tsx rename to apps/dashboard/src/app/team/components/Analytics/EmptyState.tsx index 325f49e742a..ea60f5ba4d6 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/EmptyState.tsx +++ b/apps/dashboard/src/app/team/components/Analytics/EmptyState.tsx @@ -6,17 +6,18 @@ 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 { ArrowRightIcon } from "lucide-react"; import Image, { type StaticImageData } from "next/image"; import Link from "next/link"; -import accountAbstractionIcon from "../../../../../../public/assets/tw-icons/account-abstraction.svg"; -import authIcon from "../../../../../../public/assets/tw-icons/auth.svg"; -import payIcon from "../../../../../../public/assets/tw-icons/pay.svg"; -import socialAuthIcon from "../../../../../../public/assets/tw-icons/social-auth.svg"; -import walletsIcon from "../../../../../../public/assets/tw-icons/wallets.svg"; +import accountAbstractionIcon from "../../../../../public/assets/tw-icons/account-abstraction.svg"; +import authIcon from "../../../../../public/assets/tw-icons/auth.svg"; +import payIcon from "../../../../../public/assets/tw-icons/pay.svg"; +import socialAuthIcon from "../../../../../public/assets/tw-icons/social-auth.svg"; +import walletsIcon from "../../../../../public/assets/tw-icons/wallets.svg"; export function EmptyState() { return ( -
+
@@ -62,9 +63,9 @@ export function EmptyState() {
-
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/PieChart.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/PieChart.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx b/apps/dashboard/src/app/team/components/Analytics/PieChart.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChart.tsx rename to apps/dashboard/src/app/team/components/Analytics/PieChart.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/PieChartCard.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/PieChartCard.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx b/apps/dashboard/src/app/team/components/Analytics/PieChartCard.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/PieChartCard.tsx rename to apps/dashboard/src/app/team/components/Analytics/PieChartCard.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Stat.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/Stat.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Stat.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/Stat.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Stat.tsx b/apps/dashboard/src/app/team/components/Analytics/Stat.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Stat.tsx rename to apps/dashboard/src/app/team/components/Analytics/Stat.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/StatBreakdownCard.stories.tsx b/apps/dashboard/src/app/team/components/Analytics/StatBreakdownCard.stories.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/StatBreakdownCard.stories.tsx rename to apps/dashboard/src/app/team/components/Analytics/StatBreakdownCard.stories.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/StatBreakdownCard.tsx b/apps/dashboard/src/app/team/components/Analytics/StatBreakdownCard.tsx similarity index 100% rename from apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/StatBreakdownCard.tsx rename to apps/dashboard/src/app/team/components/Analytics/StatBreakdownCard.tsx diff --git a/apps/dashboard/src/components/dashboard/HomeProductCard.tsx b/apps/dashboard/src/components/dashboard/HomeProductCard.tsx deleted file mode 100644 index 1b00738a170..00000000000 --- a/apps/dashboard/src/components/dashboard/HomeProductCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; -import type { SectionItemProps } from "components/product-pages/common/nav/types"; -import { useTrack } from "hooks/analytics/useTrack"; -import Image from "next/image"; -import Link from "next/link"; - -interface HomeProductCardProps { - product: SectionItemProps; - isFromLandingPage?: boolean; - TRACKING_CATEGORY: string; -} - -export const HomeProductCard: React.FC = ({ - product, - TRACKING_CATEGORY, - isFromLandingPage, -}) => { - const trackEvent = useTrack(); - const href = (isFromLandingPage ? product.link : product.dashboardLink) || ""; - return ( -
- {product.icon && ( -
- -
- )} -
- { - trackEvent({ - category: TRACKING_CATEGORY, - action: "click", - label: "select-product", - product: product.name, - }); - }} - > - {isFromLandingPage - ? product.name - : product?.dashboardName || product.name} - -

- {product.description} -

-
-
- ); -}; diff --git a/apps/dashboard/src/components/onboarding/Steps.tsx b/apps/dashboard/src/components/onboarding/Steps.tsx deleted file mode 100644 index 6bcce413ab1..00000000000 --- a/apps/dashboard/src/components/onboarding/Steps.tsx +++ /dev/null @@ -1,329 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { useDashboardRouter } from "@/lib/DashboardRouter"; -import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet"; -import { - accountStatus, - useAccount, - useAccountCredits, - useApiKeys, -} from "@3rdweb-sdk/react/hooks/useApi"; -import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser"; -import { OPSponsoredChains } from "constants/chains"; -import { useTrack } from "hooks/analytics/useTrack"; -import { useLocalStorage } from "hooks/useLocalStorage"; -import { ExternalLinkIcon } from "lucide-react"; -import { useTheme } from "next-themes"; -import type { StaticImageData } from "next/image"; -import Image from "next/image"; -import Link from "next/link"; -import { type JSX, useEffect, useMemo } from "react"; -import { useActiveWalletChain } from "thirdweb/react"; - -const Step = { - keys: "keys", - docs: "docs", - optimismCredits: "optimismCredits", - payment: "payment", -} as const; - -type StepId = keyof typeof Step; - -type StepData = { - key: StepId; - title: string; - description: string | JSX.Element; - cta: string; - learnMore?: string; - onClick?: () => void; - href?: string; - canSkip?: true; - rightImageLight?: StaticImageData; - rightImageDark?: StaticImageData; -}; - -interface OnboardingStepsProps { - onlyOptimism?: boolean; -} - -export const OnboardingSteps: React.FC = ({ - onlyOptimism, -}) => { - const { isLoggedIn } = useLoggedInUser(); - const meQuery = useAccount(); - const apiKeysQuery = useApiKeys(); - const router = useDashboardRouter(); - const trackEvent = useTrack(); - const { theme } = useTheme(); - const { data: credits } = useAccountCredits(); - const opCredit = credits?.find((crd) => crd.name.startsWith("OP -")); - const [onboardingPaymentMethod, setOnboardingPaymentMethod] = useLocalStorage( - `onboardingPaymentMethod-${meQuery?.data?.id}`, - false, - ); - const [onboardingKeys, setOnboardingKeys] = useLocalStorage( - `onboardingKeys-${meQuery?.data?.id}`, - false, - ); - const [onboardingDocs, setOnboardingDocs] = useLocalStorage( - `onboardingDocs-${meQuery?.data?.id}`, - false, - ); - const [hasAppliedForOpGrant] = useLocalStorage( - `appliedForOpGrant-${meQuery?.data?.id}`, - false, - ); - - const hasValidPayment = useMemo(() => { - return meQuery?.data?.status === accountStatus.validPayment; - }, [meQuery?.data?.status]); - - const hasApiKeys = useMemo(() => { - return apiKeysQuery?.data && apiKeysQuery?.data?.length > 0; - }, [apiKeysQuery?.data]); - - const chainId = useActiveWalletChain()?.id; - - const isSponsoredChain = useMemo(() => { - if (chainId) { - return OPSponsoredChains.includes(chainId); - } - }, [chainId]); - - const currentStep: StepId | null = useMemo(() => { - if (onlyOptimism && (!hasAppliedForOpGrant || !opCredit)) { - return Step.optimismCredits; - } - - if (!isLoggedIn) { - return null; - } - - if (isSponsoredChain && (!hasAppliedForOpGrant || !opCredit)) { - return Step.optimismCredits; - } - if (!onboardingKeys && !hasApiKeys) { - return Step.keys; - } - if (!hasValidPayment && !onboardingPaymentMethod) { - return Step.payment; - } - if (!onboardingDocs) { - return Step.docs; - } - return null; - }, [ - isLoggedIn, - hasApiKeys, - hasValidPayment, - onboardingDocs, - onboardingKeys, - onboardingPaymentMethod, - hasAppliedForOpGrant, - onlyOptimism, - isSponsoredChain, - opCredit, - ]); - - const handleStep = ({ - isSkip, - step, - href, - onClick, - }: { - isSkip?: true; - step: StepId; - href?: string; - onClick?: () => void; - }) => { - if (!step) { - return; - } - - if (!isSkip && href) { - if (!href.startsWith("http")) { - router.push(href); - } else { - window.open(href, "_blank"); - } - } - - if (!isSkip && onClick) { - onClick(); - } - - if (step === Step.keys) { - setOnboardingKeys(true); - } - - if (step === Step.docs) { - setOnboardingDocs(true); - } - - if (step === Step.payment) { - setOnboardingPaymentMethod(true); - } - - trackEvent({ - category: "onboardingChecklist", - action: isSkip ? "skipped" : "completed", - data: { step, href }, - }); - }; - - // TODO: find better way to track impressions - // eslint-disable-next-line no-restricted-syntax - useEffect(() => { - if (currentStep) { - trackEvent({ - category: "onboardingChecklist", - action: "viewed", - data: { step: currentStep }, - }); - } - }, [currentStep, trackEvent]); - - const STEPS: StepData[] = useMemo( - () => [ - { - key: Step.keys, - title: "Create a Project", - description: - "An Client ID or Secret Key is required to use thirdweb's services through the SDK and CLI. You can create get one by creating a project", - cta: "Create Project", - href: "/team/~/~/projects", - canSkip: true, - }, - { - key: Step.payment, - title: "Add Payment Method", - description: - "Add your payment method to ensure no disruption to thirdweb services when you exceed free monthly limits.", - cta: "Add payment", - href: "/team/~/~/settings/billing", - canSkip: true, - }, - { - key: Step.optimismCredits, - title: "Apply to join the Optimism Superchain App Accelerator!", - description: ( -

- Successful applicants will receive gas grants which can be used - across all supported{" "} - - Optimism Superchain networks - - . These can be used with our Account Abstraction tools to sponsor - gas fees for any on-chain activity. -

- ), - cta: "Apply now", - onClick: () => { - trackEvent({ - category: "onboardingChecklist", - action: "clicked", - data: { step: Step.optimismCredits }, - }); - }, - href: "/team/~/~/settings/credits", - learnMore: - "https://blog.thirdweb.com/accelerating-the-superchain-with-optimism", - rightImageDark: require("../../../public/assets/dashboard/optimism-credits-dark.png"), - rightImageLight: require("../../../public/assets/dashboard/optimism-credits-light.png"), - }, - { - key: Step.docs, - title: "Explore Docs", - description: - "Read our documentation to learn what you can build with contracts, payments, wallets, and infrastructure.", - cta: "Read docs", - href: "https://portal.thirdweb.com", - canSkip: true, - }, - ], - [trackEvent], - ); - - if (!currentStep) { - return null; - } - - const { - title, - description, - cta, - href, - learnMore, - onClick, - canSkip, - rightImageDark, - rightImageLight, - } = STEPS.find((s) => s.key === currentStep) as StepData; - - return ( -
-
-

{title}

-

{description}

-
- -
- {isLoggedIn ? ( - - ) : ( - - )} - - {learnMore && ( - - )} - - {canSkip && ( - - )} -
-
- - {rightImageDark && theme === "dark" && ( - - )} - - {rightImageLight && theme === "light" && ( - - )} -
- ); -}; diff --git a/apps/dashboard/src/stories/stubs.ts b/apps/dashboard/src/stories/stubs.ts index 82b6f2faefd..4fe910e5640 100644 --- a/apps/dashboard/src/stories/stubs.ts +++ b/apps/dashboard/src/stories/stubs.ts @@ -15,7 +15,7 @@ import type { } from "@3rdweb-sdk/react/hooks/useEngine"; import { ZERO_ADDRESS } from "thirdweb"; -export function projectStub(id: string, teamId: string) { +function projectStub(id: string, teamId: string) { const project: Project = { bundleIds: [] as string[], createdAt: new Date(),