diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index d74b4aa8450..9aee2c9facd 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "dev": "next dev --turbopack", + "dev": "next dev", "build": "NODE_OPTIONS=--max-old-space-size=6144 next build", "start": "next start", "format": "biome format ./src --write", diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/UsagePage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/UsagePage.tsx deleted file mode 100644 index b3be5c9cc84..00000000000 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/UsagePage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { useAccount, useAccountUsage } from "@3rdweb-sdk/react/hooks/useApi"; -import { BillingPeriod } from "components/settings/Account/Billing/Period"; -import { BillingPlan } from "components/settings/Account/Billing/Plan"; -import { Usage } from "components/settings/Account/Usage"; - -export const SettingsUsagePage = () => { - const meQuery = useAccount(); - const usageQuery = useAccountUsage(); - const account = meQuery.data; - - if (meQuery.isPending || !account) { - return ( -
- -
- ); - } - - return ( -
-
-

Usage

-
- - -
-
- - -
- ); -}; diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts new file mode 100644 index 00000000000..1fad3d1934c --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts @@ -0,0 +1,26 @@ +import { API_SERVER_URL } from "@/constants/env"; +import type { UsageBillableByService } from "@3rdweb-sdk/react/hooks/useApi"; +import { getAuthToken } from "../../../../../api/lib/getAuthToken"; + +export async function getAccountUsage() { + const token = await getAuthToken(); + + if (!token) { + return undefined; + } + + const res = await fetch(`${API_SERVER_URL}/v1/account/usage`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok) { + return undefined; + } + + const json = await res.json(); + return json.data as UsageBillableByService; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx new file mode 100644 index 00000000000..c69825da0d4 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx @@ -0,0 +1,132 @@ +import type { UsageBillableByService } from "@3rdweb-sdk/react/hooks/useApi"; +import { useMemo } from "react"; +import { toNumber, toPercent, toSize } from "utils/number"; +import { UsageCard } from "./UsageCard"; + +interface UsageProps { + usage: UsageBillableByService; +} + +export const Usage: React.FC = ({ usage: usageData }) => { + const bundlerMetrics = useMemo(() => { + const metric = { + title: "Total sponsored fees", + total: 0, + }; + + if (!usageData) { + return metric; + } + + return { + title: metric.title, + total: usageData.billableUsd.bundler, + }; + }, [usageData]); + + const storageMetrics = useMemo(() => { + if (!usageData) { + return {}; + } + + const consumedBytes = usageData.usage.storage.sumFileSizeBytes; + const limitBytes = usageData.limits.storage; + const percent = toPercent(consumedBytes, limitBytes); + + return { + total: `${toSize(consumedBytes, "MB")} / ${toSize( + limitBytes, + )} (${percent}%)`, + progress: percent, + ...(usageData.billableUsd.storage > 0 + ? { + overage: usageData.billableUsd.storage, + } + : {}), + }; + }, [usageData]); + + const walletsMetrics = useMemo(() => { + if (!usageData) { + return {}; + } + + 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, + } + : {}), + }; + }, [usageData]); + + const rpcMetrics = useMemo(() => { + if (!usageData) { + return {}; + } + + return { + title: "Unlimited requests", + total: ( + + {usageData.rateLimits.rpc} Requests Per Second + + ), + }; + }, [usageData]); + + const gatewayMetrics = useMemo(() => { + if (!usageData) { + return {}; + } + + return { + title: "Unlimited requests", + total: ( + + {usageData.rateLimits.storage} Requests Per Second + + ), + }; + }, [usageData]); + + return ( +
+
+ + + + + +
+
+ ); +}; diff --git a/apps/dashboard/src/components/settings/Account/UsageCard.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/UsageCard.tsx similarity index 74% rename from apps/dashboard/src/components/settings/Account/UsageCard.tsx rename to apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/UsageCard.tsx index 88269eca370..fe396c39301 100644 --- a/apps/dashboard/src/components/settings/Account/UsageCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/UsageCard.tsx @@ -1,9 +1,8 @@ import { ToolTipLabel } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { CircleHelpIcon } from "lucide-react"; -import { toUSD } from "utils/number"; - import type { JSX } from "react"; +import { toUSD } from "utils/number"; interface UsageCardProps { name: string; @@ -23,20 +22,18 @@ export const UsageCard: React.FC = ({ tooltip, }) => { return ( -
-
-

{name}

- {tooltip && ( - - - - )} -
+
+

{name}

+ {tooltip && ( + + + + )}
-
- {title &&

{title}

} +
+ {title &&

{title}

} {total !== undefined && (

diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/page.tsx index ae8a3158fad..7530c6ec28b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/page.tsx @@ -1,12 +1,150 @@ -import { ChakraProviderSetup } from "@/components/ChakraProviderSetup"; -import { SettingsUsagePage } from "./UsagePage"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import type { + Account, + UsageBillableByService, +} from "@3rdweb-sdk/react/hooks/useApi"; +import { format } from "date-fns/format"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { PLANS } from "utils/pricing"; +import { getAccount } from "../../../../../account/settings/getAccount"; +import { getAccountUsage } from "./getAccountUsage"; +import { Usage } from "./overview/components/Usage"; + +export default async function Page(props: { + params: Promise<{ + team_slug: string; + }>; +}) { + const params = await props.params; + const account = await getAccount(); + + if (!account) { + return redirect( + `/login?next=${encodeURIComponent(`/team/${params.team_slug}/~/usage`)}`, + ); + } + + const accountUsage = await getAccountUsage(); + if (!accountUsage) { + return ( +

+ Something went wrong. Please try again later. +
+ ); + } + + return ( +
+ + +
+ ); +} + +function PlanInfoCard(props: { + account: Account; + accountUsage: UsageBillableByService; + team_slug: string; +}) { + const { account, accountUsage } = props; + + return ( +
+
+

+ {PLANS[account.plan as keyof typeof PLANS].title} Plan +

+ +
+ + + +
+
+ + + +
+ +
+
+ ); +} + +function BillingInfo({ + account, + usage, +}: { + account: Account; + usage: UsageBillableByService; +}) { + if ( + !account.currentBillingPeriodStartsAt || + !account.currentBillingPeriodEndsAt + ) { + return null; + } + + const totalUsd = getBillingAmountInUSD(usage); -export default function Page() { return ( - -
- +
+
+
Current Billing Cycle
+

+ {format( + new Date(account.currentBillingPeriodStartsAt), + "MMMM dd yyyy", + )}{" "} + -{" "} + {format( + new Date(account.currentBillingPeriodEndsAt), + "MMMM dd yyyy", + )}{" "} +

- + + + +
+
Total Upcoming Bill
+

{totalUsd}

+
+
); } + +function getBillingAmountInUSD(usage: UsageBillableByService) { + let total = 0; + + if (usage.billableUsd) { + for (const amount of Object.values(usage.billableUsd)) { + total += amount; + } + } + + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: "USD", + }).format(total); +} diff --git a/apps/dashboard/src/components/settings/Account/Billing/Period.tsx b/apps/dashboard/src/components/settings/Account/Billing/Period.tsx deleted file mode 100644 index 36d30b32ba2..00000000000 --- a/apps/dashboard/src/components/settings/Account/Billing/Period.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { - Account, - UsageBillableByService, -} from "@3rdweb-sdk/react/hooks/useApi"; -import { format } from "date-fns/format"; -import { useMemo } from "react"; -import { Text } from "tw-components"; - -interface BillingPeriodProps { - account: Account; - usage: UsageBillableByService | undefined; -} - -export const BillingPeriod: React.FC = ({ - account, - usage, -}) => { - const totalUsd = useMemo(() => { - let total = 0; - - // biome-ignore lint/complexity/noForEach: FIXME - Object.values(usage?.billableUsd || {}).forEach((amount) => { - total += amount; - }); - - return new Intl.NumberFormat(undefined, { - style: "currency", - currency: "USD", - }).format(total); - }, [usage]); - - if ( - !account.currentBillingPeriodStartsAt || - !account.currentBillingPeriodEndsAt - ) { - return null; - } - - return ( -
- - Current billing period: - - {format( - new Date(account.currentBillingPeriodStartsAt as string), - "MMM dd", - )}{" "} - - - {format( - new Date(account.currentBillingPeriodEndsAt as string), - "MMM dd", - )}{" "} - - - - - Total upcoming bill: - - {totalUsd} - - -
- ); -}; diff --git a/apps/dashboard/src/components/settings/Account/Billing/Plan.tsx b/apps/dashboard/src/components/settings/Account/Billing/Plan.tsx deleted file mode 100644 index cc89a3ec815..00000000000 --- a/apps/dashboard/src/components/settings/Account/Billing/Plan.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; -import { Flex } from "@chakra-ui/react"; -import { Badge, Text, TrackedLink } from "tw-components"; -import { PLANS } from "utils/pricing"; - -interface BillingPlanProps { - account: Account; - direction?: "column" | "row"; - description?: string; - titleSize?: "body.md" | "label.lg"; - titleColor?: string; -} - -export const BillingPlan: React.FC = ({ - account, - description, - titleSize = "body.md", - titleColor = "GrayText", - direction = "row", -}) => { - return ( - -
- - Your current plan is - - - {PLANS[account.plan as keyof typeof PLANS].title} - -
- -
- {description && {description}} - - - - Learn more - - -
-
- ); -}; diff --git a/apps/dashboard/src/components/settings/Account/Usage.tsx b/apps/dashboard/src/components/settings/Account/Usage.tsx deleted file mode 100644 index e847766341c..00000000000 --- a/apps/dashboard/src/components/settings/Account/Usage.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import type { UsageBillableByService } from "@3rdweb-sdk/react/hooks/useApi"; -import { SimpleGrid, Spinner } from "@chakra-ui/react"; -import { useMemo } from "react"; -import { Heading, Text } from "tw-components"; -import { toNumber, toPercent, toSize } from "utils/number"; -import { UsageCard } from "./UsageCard"; - -interface UsageProps { - usage: UsageBillableByService | undefined; - usagePending: boolean; -} - -export const Usage: React.FC = ({ - usage: usageData, - usagePending, -}) => { - const bundlerMetrics = useMemo(() => { - const metric = { - title: "Total sponsored fees", - total: 0, - }; - - if (!usageData) { - return metric; - } - - return { - title: metric.title, - total: usageData.billableUsd.bundler, - }; - }, [usageData]); - - const storageMetrics = useMemo(() => { - if (!usageData) { - return {}; - } - - const consumedBytes = usageData.usage.storage.sumFileSizeBytes; - const limitBytes = usageData.limits.storage; - const percent = toPercent(consumedBytes, limitBytes); - - return { - total: `${toSize(consumedBytes, "MB")} / ${toSize( - limitBytes, - )} (${percent}%)`, - progress: percent, - ...(usageData.billableUsd.storage > 0 - ? { - overage: usageData.billableUsd.storage, - } - : {}), - }; - }, [usageData]); - - const walletsMetrics = useMemo(() => { - if (!usageData) { - return {}; - } - - 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, - } - : {}), - }; - }, [usageData]); - - const rpcMetrics = useMemo(() => { - if (!usageData) { - return {}; - } - - return { - title: "Unlimited requests", - total: ( - <> - Max rate:{" "} - - {usageData.rateLimits.rpc} requests per second - - - ), - }; - }, [usageData]); - - const gatewayMetrics = useMemo(() => { - if (!usageData) { - return {}; - } - - return { - title: "Unlimited requests", - total: ( - <> - Max rate: - - {usageData.rateLimits.storage} requests per second - - - ), - }; - }, [usageData]); - - return ( -
- {usagePending && } - {!usagePending && ( -
-
- - Infrastructure - - - - - - - -
- -
- - Wallets - - - - - -
- -
- - Payments - - - - - -
-
- )} -
- ); -}; diff --git a/apps/dashboard/src/utils/pricing.tsx b/apps/dashboard/src/utils/pricing.tsx index 43f4b37c901..5f962dae908 100644 --- a/apps/dashboard/src/utils/pricing.tsx +++ b/apps/dashboard/src/utils/pricing.tsx @@ -1,5 +1,5 @@ import { type AccountPlan, accountPlan } from "@3rdweb-sdk/react/hooks/useApi"; -import { Link, Text } from "tw-components"; +import Link from "next/link"; export const CONTACT_US_URL = "https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro"; @@ -173,75 +173,81 @@ export const FAQ_GENERAL = [ { title: "How do I get started?", description: ( - + thirdweb Starter plan is completely usage based. Simply connect your wallet to start using thirdweb platform. You only need to create an account with your email address and add payment method when you're approaching your monthly free usage credits (so that we can send you billing updates if you go over). - + ), }, { title: "Which plan is right for me?", description: ( - + If you are looking for production grade infrastructure, advanced customizations, and higher limits for transactions, Growth tier is the right choice. If you are looking for dedicated solutions and support SLAs, we recommend signing up for the Pro plan. - + ), }, { title: "Do I need to talk to the sales team for the Growth plan?", description: ( - + Nope! You can self serve and upgrade to the Growth plan in the Dashboard under{" "} - + Billing {" "} whenever you are ready! - + ), }, { title: "Will I be able to see my usage history?", description: ( - + You can review your usage history at any time on the Dashboard by visiting the{" "} - + Usage {" "} tab under Settings. - + ), }, { title: "How is pricing calculated for in-app wallets?", description: ( - + In-App wallets are billed based on "Monthly active wallets". - + ), }, { title: "What is a Monthly Active Wallet?", description: ( - + A Monthly Active Wallet is defined as a wallet where a user logs in during the billing period. - + ), }, { title: "Do you have an implementation fee?", description: ( - + No, we do not have any implementation fees for any of our plans. - + ), }, ]; @@ -250,38 +256,38 @@ export const FAQ_PRICING = [ { title: "RPC requests", description: ( - + When your app makes requests to the blockchain, and you use thirdweb's built-in infrastructure, it will count as a RPC request. - + ), }, { title: "Storage gateway", description: ( - + When your app downloads files from IPFS, and you use thirdweb's built-in infrastructure, it will count as a storage gateway request. - + ), }, { title: "Storage pinning", description: ( - + When your app uploads files to IPFS, and you use thirdweb's built-in infrastructure, it will count towards your storage pinning limit. - + ), }, { title: "Monthly Active Wallet", description: ( - + When a user logs in during a 30-day period in using the in-app wallet service, they are counted as a monthly active wallet. - + ), }, ];