diff --git a/apps/dashboard/src/@/actions/billing.ts b/apps/dashboard/src/@/actions/billing.ts index 7860f429e7d..1abca004b60 100644 --- a/apps/dashboard/src/@/actions/billing.ts +++ b/apps/dashboard/src/@/actions/billing.ts @@ -1,7 +1,10 @@ "use server"; +import "server-only"; import { getAuthToken } from "@/api/auth-token"; import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; +import type { ChainInfraSKU } from "@/types/billing"; +import { getAbsoluteUrl } from "@/utils/vercel"; export async function reSubscribePlan(options: { teamId: string; @@ -14,7 +17,10 @@ export async function reSubscribePlan(options: { } const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`, + new URL( + `/v1/teams/${options.teamId}/checkout/resubscribe-plan`, + NEXT_PUBLIC_THIRDWEB_API_HOST, + ), { body: JSON.stringify({}), headers: { @@ -35,3 +41,83 @@ export async function reSubscribePlan(options: { status: 200, }; } + +export async function getChainInfraCheckoutURL(options: { + teamSlug: string; + skus: ChainInfraSKU[]; + chainId: number; + annual: boolean; +}) { + const token = await getAuthToken(); + + if (!token) { + return { + error: "You are not logged in", + status: "error", + } as const; + } + + const res = await fetch( + new URL( + `/v1/teams/${options.teamSlug}/checkout/create-link`, + NEXT_PUBLIC_THIRDWEB_API_HOST, + ), + { + body: JSON.stringify({ + annual: options.annual, + baseUrl: getAbsoluteUrl(), + chainId: options.chainId, + skus: options.skus, + }), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", + }, + ); + if (!res.ok) { + const text = await res.text(); + console.error("Failed to create checkout link", text, res.status); + switch (res.status) { + case 402: { + return { + error: + "You have outstanding invoices, please pay these first before re-subscribing.", + status: "error", + } as const; + } + case 429: { + return { + error: "Too many requests, please try again later.", + status: "error", + } as const; + } + case 403: { + return { + error: "You are not authorized to deploy infrastructure.", + status: "error", + } as const; + } + default: { + return { + error: "An unknown error occurred, please try again later.", + status: "error", + } as const; + } + } + } + + const json = await res.json(); + if (!json.result) { + return { + error: "An unknown error occurred, please try again later.", + status: "error", + } as const; + } + + return { + data: json.result as string, + status: "success", + } as const; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/types.ts b/apps/dashboard/src/@/api/ecosystems.ts similarity index 59% rename from apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/types.ts rename to apps/dashboard/src/@/api/ecosystems.ts index 5db9b603617..1f4793d004f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/types.ts +++ b/apps/dashboard/src/@/api/ecosystems.ts @@ -1,22 +1,26 @@ -export const authOptions = [ - "email", - "phone", - "passkey", - "siwe", - "guest", - "google", - "facebook", - "x", - "discord", - "farcaster", - "telegram", - "github", - "twitch", - "steam", - "apple", - "coinbase", - "line", -] as const; +import "server-only"; + +import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; +import { getAuthToken } from "./auth-token"; + +export type AuthOption = + | "email" + | "phone" + | "passkey" + | "siwe" + | "guest" + | "google" + | "facebook" + | "x" + | "discord" + | "farcaster" + | "telegram" + | "github" + | "twitch" + | "steam" + | "apple" + | "coinbase" + | "line"; export type Ecosystem = { name: string; @@ -24,7 +28,7 @@ export type Ecosystem = { id: string; slug: string; permission: "PARTNER_WHITELIST" | "ANYONE"; - authOptions: (typeof authOptions)[number][]; + authOptions: AuthOption[]; customAuthOptions?: { authEndpoint?: { url: string; @@ -47,6 +51,54 @@ export type Ecosystem = { updatedAt: string; }; +export async function fetchEcosystemList(teamIdOrSlug: string) { + const token = await getAuthToken(); + + if (!token) { + return []; + } + + const res = await fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!res.ok) { + return []; + } + + return (await res.json()).result as Ecosystem[]; +} + +export async function fetchEcosystem(slug: string, teamIdOrSlug: string) { + const token = await getAuthToken(); + + if (!token) { + return null; + } + + const res = await fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + if (!res.ok) { + const data = await res.json(); + console.error(data); + return null; + } + + const data = (await res.json()) as { result: Ecosystem }; + return data.result; +} + type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1"; export type Partner = { id: string; diff --git a/apps/dashboard/src/@/api/team-subscription.ts b/apps/dashboard/src/@/api/team-subscription.ts index 05858f9acef..ad183e8fe33 100644 --- a/apps/dashboard/src/@/api/team-subscription.ts +++ b/apps/dashboard/src/@/api/team-subscription.ts @@ -1,6 +1,6 @@ import { getAuthToken } from "@/api/auth-token"; import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; -import type { ProductSKU } from "@/types/billing"; +import type { ChainInfraSKU, ProductSKU } from "@/types/billing"; type InvoiceLine = { // amount for this line item @@ -22,7 +22,7 @@ type Invoice = { export type TeamSubscription = { id: string; - type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT"; + type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN"; status: | "incomplete" | "incomplete_expired" @@ -37,6 +37,13 @@ export type TeamSubscription = { trialStart: string | null; trialEnd: string | null; upcomingInvoice: Invoice; + skus: (ProductSKU | ChainInfraSKU)[]; +}; + +type ChainTeamSubscription = Omit & { + chainId: string; + skus: ChainInfraSKU[]; + isLegacy: boolean; }; export async function getTeamSubscriptions(slug: string) { @@ -60,3 +67,61 @@ export async function getTeamSubscriptions(slug: string) { } return null; } + +const CHAIN_PLAN_TO_INFRA = { + "chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"], + "chain:plan:platinum": [ + "chain:infra:rpc", + "chain:infra:insight", + "chain:infra:account_abstraction", + ], + "chain:plan:ultimate": [ + "chain:infra:rpc", + "chain:infra:insight", + "chain:infra:account_abstraction", + ], +}; + +export async function getChainSubscriptions(slug: string) { + const allSubscriptions = await getTeamSubscriptions(slug); + if (!allSubscriptions) { + return null; + } + + // first replace any sku that MIGHT match a chain plan + const updatedSubscriptions = allSubscriptions + .filter((s) => s.type === "CHAIN") + .map((s) => { + const skus = s.skus; + const updatedSkus = skus.flatMap((sku) => { + const plan = + CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA]; + return plan ? plan : sku; + }); + return { + ...s, + isLegacy: updatedSkus.length !== skus.length, + skus: updatedSkus, + }; + }); + + return updatedSubscriptions.filter( + (s): s is ChainTeamSubscription => + "chainId" in s && typeof s.chainId === "string", + ); +} + +export async function getChainSubscriptionForChain( + slug: string, + chainId: number, +) { + const chainSubscriptions = await getChainSubscriptions(slug); + + if (!chainSubscriptions) { + return null; + } + + return ( + chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null + ); +} diff --git a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx index 48091acc635..e6507f45d6d 100644 --- a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx +++ b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx @@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: { disableChainId?: boolean; align?: "center" | "start" | "end"; disableTestnets?: boolean; + disableDeprecated?: boolean; placeholder?: string; client: ThirdwebClient; }) { @@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: { chains = chains.filter((chain) => chainIdSet.has(chain.chainId)); } + if (props.disableDeprecated) { + chains = chains.filter((chain) => chain.status !== "deprecated"); + } + return chains; - }, [allChains, props.chainIds, props.disableTestnets]); + }, [ + allChains, + props.chainIds, + props.disableTestnets, + props.disableDeprecated, + ]); const options = useMemo(() => { return chainsToShow.map((chain) => { diff --git a/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx index 9050c2854de..a9461a8a6e8 100644 --- a/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx +++ b/apps/dashboard/src/@/components/blocks/UpsellBannerCard.tsx @@ -38,12 +38,18 @@ const ACCENT = { type UpsellBannerCardProps = { title: React.ReactNode; description: React.ReactNode; - cta: { - text: React.ReactNode; - icon?: React.ReactNode; - target?: "_blank"; - link: string; - }; + cta?: + | { + text: React.ReactNode; + icon?: React.ReactNode; + target?: "_blank"; + link: string; + } + | { + text: React.ReactNode; + icon?: React.ReactNode; + onClick: () => void; + }; accentColor?: keyof typeof ACCENT; icon?: React.ReactNode; }; @@ -93,25 +99,41 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) { - + ) : props.cta && "onClick" in props.cta ? ( + + + ) : null} ); diff --git a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx index 17958a9b3f9..91e13fac006 100644 --- a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx +++ b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx @@ -279,7 +279,14 @@ function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) { // subnav if ("subMenu" in link) { return ( - + ); } diff --git a/apps/dashboard/src/@/icons/ChainIcon.tsx b/apps/dashboard/src/@/icons/ChainIcon.tsx index ae845a75915..fef285a6635 100644 --- a/apps/dashboard/src/@/icons/ChainIcon.tsx +++ b/apps/dashboard/src/@/icons/ChainIcon.tsx @@ -30,7 +30,7 @@ export const ChainIconClient = ({ fallback={} key={resolvedSrc} loading={restProps.loading || "lazy"} - skeleton={
} + skeleton={} src={resolvedSrc} /> ); diff --git a/apps/dashboard/src/@/storybook/stubs.ts b/apps/dashboard/src/@/storybook/stubs.ts index 0d02432b5a8..1308233de4e 100644 --- a/apps/dashboard/src/@/storybook/stubs.ts +++ b/apps/dashboard/src/@/storybook/stubs.ts @@ -192,6 +192,7 @@ export function teamSubscriptionsStub( currentPeriodEnd: "2024-12-15T20:56:06.000Z", currentPeriodStart: "2024-11-15T20:56:06.000Z", id: "sub-1", + skus: [], status: "active", trialEnd: overrides?.trialEnd || null, trialStart: null, @@ -212,6 +213,7 @@ export function teamSubscriptionsStub( currentPeriodEnd: "2024-12-15T20:56:06.000Z", currentPeriodStart: "2024-11-15T20:56:15.000Z", id: "sub-2", + skus: [], status: "active", trialEnd: null, trialStart: null, @@ -229,25 +231,19 @@ export function teamSubscriptionsStub( // In-App Wallets { amount: usage.inAppWalletAmount?.amount || 0, - description: `${ - usage.inAppWalletAmount?.quantity || 0 - } x In-App Wallets (Tier 1 at $0.00 / month)`, + description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`, thirdwebSku: "usage:in_app_wallet", }, // AA Sponsorship { amount: usage.aaSponsorshipAmount?.amount || 0, - description: `${ - usage.aaSponsorshipAmount?.quantity || 0 - } x AA Gas Sponsorship (at $0.011 / month)`, + description: `${usage.aaSponsorshipAmount?.quantity || 0} x AA Gas Sponsorship (at $0.011 / month)`, thirdwebSku: "usage:aa_sponsorship", }, // OP Grant { amount: usage.aaSponsorshipOpGrantAmount?.amount || 0, - description: `${ - usage.aaSponsorshipOpGrantAmount?.quantity || 0 - } x AA Gas Sponsorship (OP) (at $0.011 / month)`, + description: `${usage.aaSponsorshipOpGrantAmount?.quantity || 0} x AA Gas Sponsorship (OP) (at $0.011 / month)`, thirdwebSku: "usage:aa_sponsorship_op_grant", }, ], diff --git a/apps/dashboard/src/@/types/billing.ts b/apps/dashboard/src/@/types/billing.ts index 992af032853..cfa6f45df3d 100644 --- a/apps/dashboard/src/@/types/billing.ts +++ b/apps/dashboard/src/@/types/billing.ts @@ -14,3 +14,8 @@ export type ProductSKU = | "usage:aa_sponsorship" | "usage:aa_sponsorship_op_grant" | null; + +export type ChainInfraSKU = + | "chain:infra:rpc" + | "chain:infra:insight" + | "chain:infra:account_abstraction"; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/explore/components/contract-row/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/explore/components/contract-row/index.tsx index d9d11c088f7..95b1ac200d6 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/explore/components/contract-row/index.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/explore/components/contract-row/index.tsx @@ -5,7 +5,7 @@ import { Suspense } from "react"; import { ContractCard, ContractCardSkeleton, -} from "../../../../../../@/components/contracts/contract-card"; +} from "@/components/contracts/contract-card"; interface ContractRowProps { category: ExploreCategory; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx index ce0e84d3184..d28df97442b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx @@ -16,6 +16,11 @@ export function TeamSidebarLayout(props: { layoutPath: string; children: React.ReactNode; ecosystems: Array<{ name: string; slug: string }>; + chainSubscriptions: Array<{ + chainId: number; + chainName: string; + slug: string; + }>; }) { const { layoutPath, children } = props; @@ -59,6 +64,29 @@ export function TeamSidebarLayout(props: { icon: DatabaseIcon, label: "Usage", }, + ...(props.chainSubscriptions.length > 0 + ? [ + { + separator: true, + } as const, + { + links: [ + ...props.chainSubscriptions.map((chainSubscription) => ({ + href: `${layoutPath}/~/infrastructure/${chainSubscription.slug}`, + label: chainSubscription.chainName, + })), + { + href: `${layoutPath}/~/infrastructure/deploy`, + label: "Deploy Infrastructure", + }, + ], + subMenu: { + icon: WalletCardsIcon, + label: "Chain Infrastucture", + }, + }, + ] + : []), ]} footerSidebarLinks={[ { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx index 88810bcbf4d..02cf04df252 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx @@ -1,17 +1,18 @@ import { redirect } from "next/navigation"; import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token"; +import { fetchEcosystemList } from "@/api/ecosystems"; import { getProjects } from "@/api/projects"; import { getTeamBySlug, getTeams } from "@/api/team"; +import { getChainSubscriptions } from "@/api/team-subscription"; import { CustomChatButton } from "@/components/chat/CustomChatButton"; import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner"; import { SidebarProvider } from "@/components/ui/sidebar"; -import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { getChain } from "../../../(dashboard)/(chain)/utils"; import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions"; import { getValidAccount } from "../../../account/settings/getAccount"; import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client"; import { StaffModeNotice } from "./_components/StaffModeNotice"; -import type { Ecosystem } from "./~/ecosystem/types"; import { TeamSidebarLayout } from "./TeamSidebarLayout"; export default async function TeamLayout(props: { @@ -20,32 +21,58 @@ export default async function TeamLayout(props: { }) { const params = await props.params; - const [accountAddress, account, teams, authToken, team] = await Promise.all([ + const [ + accountAddress, + account, + teams, + authToken, + team, + ecosystems, + chainSubscriptions, + ] = await Promise.all([ getAuthTokenWalletAddress(), getValidAccount(`/team/${params.team_slug}`), getTeams(), getAuthToken(), getTeamBySlug(params.team_slug), + fetchEcosystemList(params.team_slug), + getChainSubscriptions(params.team_slug), ]); if (!teams || !accountAddress || !authToken || !team) { redirect("/login"); } - const teamsAndProjects = await Promise.all( - teams.map(async (team) => ({ - projects: await getProjects(team.slug), - team, - })), - ); + const [teamsAndProjects, chainSidebarLinks] = await Promise.all([ + Promise.all( + teams.map(async (team) => ({ + projects: await getProjects(team.slug), + team, + })), + ), + chainSubscriptions + ? await Promise.all( + chainSubscriptions.map(async (chainSubscription) => { + if (!chainSubscription.chainId) { + throw new Error("Chain ID is required"); + } + const chain = await getChain(chainSubscription.chainId); + + return { + chainId: chain.chainId, + chainName: chain.name, + slug: chain.slug, + }; + }), + ).catch(() => []) + : [], + ]); const client = getClientThirdwebClient({ jwt: authToken, teamId: team.id, }); - const ecosystems = await fetchEcosystemList(team.id, authToken); - const isStaffMode = !teams.some((t) => t.slug === team.slug); return ( @@ -65,6 +92,9 @@ export default async function TeamLayout(props: {
a.chainId - b.chainId, + )} ecosystems={ecosystems.map((ecosystem) => ({ name: ecosystem.name, slug: ecosystem.slug, @@ -90,20 +120,3 @@ export default async function TeamLayout(props: { ); } - -async function fetchEcosystemList(teamId: string, authToken: string) { - const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/ecosystem-wallet`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }, - ); - - if (!res.ok) { - return []; - } - - return (await res.json()).result as Ecosystem[]; -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx index 733259536e5..ac356034326 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/billing/layout.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; import { getAuthToken } from "@/api/auth-token"; import { getTeamBySlug } from "@/api/team"; -import { TabPathLinks } from "../../../../../../../@/components/ui/tabs"; +import { TabPathLinks } from "@/components/ui/tabs"; import { loginRedirect } from "../../../../../login/loginRedirect"; export default async function Layout(props: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx index a9300402db8..be107eeedf9 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage.tsx @@ -1,10 +1,10 @@ import { getEcosystemWalletUsage } from "@/api/analytics"; +import type { Partner } from "@/api/ecosystems"; import { getLastNDaysRange, type Range, } from "@/components/analytics/date-range-selector"; import { RangeSelector } from "@/components/analytics/range-selector"; -import type { Partner } from "../../../../types"; import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard"; import { EcosystemWalletsSummary } from "./Summary"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx index 22f9da796c7..fc8596a2ada 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx @@ -1,6 +1,7 @@ "use client"; import { format } from "date-fns"; import { useMemo } from "react"; +import type { Partner } from "@/api/ecosystems"; import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; import { DocLink } from "@/components/blocks/DocLink"; import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton"; @@ -10,7 +11,6 @@ import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon"; import { UnityIcon } from "@/icons/brand-icons/UnityIcon"; import type { EcosystemWalletStats } from "@/types/analytics"; import { formatTickerNumber } from "@/utils/format-utils"; -import type { Partner } from "../../../../types"; type ChartData = Record & { time: string; // human readable date diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/page.tsx index 80f30feac97..18beeb8a73b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; +import { getAuthToken } from "@/api/auth-token"; +import { fetchEcosystem } from "@/api/ecosystems"; import { getTeamBySlug } from "@/api/team"; -import { getAuthToken } from "../../../../../../../../../../@/api/auth-token"; -import { fetchEcosystem } from "../../../utils/fetchEcosystem"; import { fetchPartners } from "../configuration/hooks/fetchPartners"; import { EcosystemAnalyticsPage } from "./components/EcosystemAnalyticsPage"; @@ -29,7 +29,7 @@ export default async function Page(props: { } const [ecosystem, team] = await Promise.all([ - fetchEcosystem(params.slug, authToken, params.team_slug), + fetchEcosystem(params.slug, params.team_slug), getTeamBySlug(params.team_slug), ]); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx index 2eab1880cab..d8ce5386c2e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx @@ -1,9 +1,9 @@ import { redirect } from "next/navigation"; import { getAuthToken } from "@/api/auth-token"; +import { fetchEcosystem } from "@/api/ecosystems"; import { getTeamBySlug } from "@/api/team"; import { TabPathLinks } from "@/components/ui/tabs"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { fetchEcosystem } from "../../../utils/fetchEcosystem"; import { EcosystemHeader } from "./ecosystem-header.client"; export async function EcosystemLayoutSlug({ @@ -21,11 +21,7 @@ export async function EcosystemLayoutSlug({ redirect(ecosystemLayoutPath); } - const ecosystem = await fetchEcosystem( - params.slug, - authToken, - params.team_slug, - ); + const ecosystem = await fetchEcosystem(params.slug, params.team_slug); // Fetch team details to obtain team ID for further authenticated updates const team = await getTeamBySlug(params.team_slug); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx index a09416faeee..a5451019399 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; -/* eslint-disable */ +import type { Ecosystem } from "@/api/ecosystems"; import { Img } from "@/components/blocks/Img"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -26,7 +26,6 @@ import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; -import type { Ecosystem } from "../../../types"; import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem"; import { useEcosystem } from "../hooks/use-ecosystem"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx index 1ddf351c36b..332cf2ea199 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/add-partner/page.tsx @@ -1,10 +1,10 @@ import { notFound } from "next/navigation"; +import { getAuthToken } from "@/api/auth-token"; +import { fetchEcosystem } from "@/api/ecosystems"; import { getTeamBySlug } from "@/api/team"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getAuthToken } from "../../../../../../../../../../../@/api/auth-token"; import { loginRedirect } from "../../../../../../../../../login/loginRedirect"; import { AddPartnerForm } from "../components/client/add-partner-form.client"; -import { fetchEcosystem } from "../hooks/fetchEcosystem"; export default async function AddPartnerPage({ params, @@ -34,11 +34,10 @@ export default async function AddPartnerPage({ }); try { - const ecosystem = await fetchEcosystem({ - authToken, - slug: ecosystemSlug, - teamIdOrSlug: teamSlug, - }); + const ecosystem = await fetchEcosystem(ecosystemSlug, teamSlug); + if (!ecosystem) { + throw new Error("Ecosystem not found"); + } return (
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx index 921de8d3a61..c01d7404a12 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/AddPartnerDialogButton.tsx @@ -2,8 +2,8 @@ import { PlusIcon } from "lucide-react"; import Link from "next/link"; +import type { Ecosystem } from "@/api/ecosystems"; import { Button } from "@/components/ui/button"; -import type { Ecosystem } from "../../../../../types"; export function AddPartnerDialogButton(props: { teamSlug: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx index c839ffb4628..c45a37c02e7 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/add-partner-form.client.tsx @@ -2,8 +2,8 @@ import { useParams } from "next/navigation"; import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; import { useDashboardRouter } from "@/lib/DashboardRouter"; -import type { Ecosystem, Partner } from "../../../../../types"; import { useAddPartner } from "../../hooks/use-add-partner"; import { PartnerForm, type PartnerFormValues } from "./partner-form.client"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx index 3a0e2fa4d04..5669c5d71c9 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx @@ -12,6 +12,7 @@ import { } from "thirdweb/wallets/smart"; import invariant from "tiny-invariant"; import { z } from "zod"; +import type { AuthOption, Ecosystem } from "@/api/ecosystems"; import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; import { SettingsCard } from "@/components/blocks/SettingsCard"; import { Button } from "@/components/ui/button"; @@ -36,9 +37,28 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; -import { authOptions, type Ecosystem } from "../../../../../types"; import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem"; +const authOptions = [ + "email", + "phone", + "passkey", + "siwe", + "guest", + "google", + "facebook", + "x", + "discord", + "farcaster", + "telegram", + "github", + "twitch", + "steam", + "apple", + "coinbase", + "line", +] as const satisfies AuthOption[]; + type AuthOptionsFormData = { authOptions: string[]; useCustomAuth: boolean; @@ -220,7 +240,7 @@ export function AuthOptionsForm({ updateEcosystem({ ...ecosystem, - authOptions: data.authOptions as (typeof authOptions)[number][], + authOptions: data.authOptions as AuthOption[], customAuthOptions, smartAccountOptions, }); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/integration-permissions-toggle.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/integration-permissions-toggle.client.tsx index 4a626708e09..6273dcc84b9 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/integration-permissions-toggle.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/integration-permissions-toggle.client.tsx @@ -2,11 +2,11 @@ import { useState } from "react"; import { toast } from "sonner"; import invariant from "tiny-invariant"; +import type { Ecosystem } from "@/api/ecosystems"; import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog"; import { RadioGroup, RadioGroupItemButton } from "@/components/ui/radio-group"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; -import type { Ecosystem } from "../../../../../types"; import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem"; export function IntegrationPermissionsToggle({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx index 23e8a914761..87727417fa2 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/partner-form.client.tsx @@ -6,6 +6,7 @@ import { useId } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import type { ThirdwebClient } from "thirdweb"; import type { z } from "zod"; +import type { Partner } from "@/api/ecosystems"; import { Button } from "@/components/ui/button"; import { Form, @@ -21,7 +22,6 @@ import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/Spinner/Spinner"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; -import type { Partner } from "../../../../../types"; import { partnerFormSchema } from "../../constants"; import { AllowedOperationsSection } from "./allowed-operations-section"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx index f49a92b5f0b..8c72fb5b111 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/update-partner-form.client.tsx @@ -2,8 +2,8 @@ import { useParams } from "next/navigation"; import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; import { useDashboardRouter } from "@/lib/DashboardRouter"; -import type { Ecosystem, Partner } from "../../../../../types"; import { useUpdatePartner } from "../../hooks/use-update-partner"; import { PartnerForm, type PartnerFormValues } from "./partner-form.client"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/auth-options-section.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/auth-options-section.tsx index 09ebd3196ec..81e6233265f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/auth-options-section.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/auth-options-section.tsx @@ -1,5 +1,5 @@ import type { ThirdwebClient } from "thirdweb"; -import type { Ecosystem } from "../../../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; import { AuthOptionsForm, AuthOptionsFormSkeleton, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx index 6407febcb5e..462149db9cf 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/ecosystem-partners-section.tsx @@ -1,4 +1,4 @@ -import type { Ecosystem } from "../../../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; import { AddPartnerDialogButton } from "../client/AddPartnerDialogButton"; import { PartnersTable } from "./partners-table"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/integration-permissions-section.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/integration-permissions-section.tsx index adf12a72f89..d359c1879f4 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/integration-permissions-section.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/integration-permissions-section.tsx @@ -1,4 +1,4 @@ -import type { Ecosystem } from "../../../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; import { IntegrationPermissionsToggle, IntegrationPermissionsToggleSkeleton, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx index e65ac9ac787..d95866aa142 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/server/partners-table.tsx @@ -1,6 +1,7 @@ import { Link } from "chakra/link"; import { PencilIcon, Trash2Icon } from "lucide-react"; import { toast } from "sonner"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; import { Button } from "@/components/ui/button"; import { ConfirmationDialog } from "@/components/ui/ConfirmationDialog"; import { CopyButton } from "@/components/ui/CopyButton"; @@ -17,7 +18,6 @@ import { import { ToolTipLabel } from "@/components/ui/tooltip"; import { useDashboardRouter } from "@/lib/DashboardRouter"; import { cn } from "@/lib/utils"; -import type { Ecosystem, Partner } from "../../../../../types"; import { usePartners } from "../../../hooks/use-partners"; import { useDeletePartner } from "../../hooks/use-delete-partner"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts deleted file mode 100644 index a01b9c5590f..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchEcosystem.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; -import type { Ecosystem } from "../../../../types"; - -/** - * Fetches ecosystem data from the server - */ -export async function fetchEcosystem(args: { - teamIdOrSlug: string; - slug: string; - authToken: string; -}): Promise { - const { teamIdOrSlug, slug, authToken } = args; - const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - next: { - revalidate: 0, - }, - }, - ); - - if (!res.ok) { - const data = await res.json(); - console.error(data); - throw new Error( - data?.message ?? data?.error?.message ?? "Failed to fetch ecosystem", - ); - } - - return (await res.json()).result as Ecosystem; -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts index aa91df3eadb..44d10822864 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartnerDetails.ts @@ -1,4 +1,4 @@ -import type { Ecosystem, Partner } from "../../../../types"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; export async function fetchPartnerDetails(args: { authToken: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts index cbdbd94ac50..9784581e2bb 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/fetchPartners.ts @@ -1,4 +1,4 @@ -import type { Ecosystem, Partner } from "../../../../types"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; /** * Fetches partners for an ecosystem diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-add-partner.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-add-partner.ts index 27bc0e0d739..ee308fc3dcb 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-add-partner.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-add-partner.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; -import type { Ecosystem, Partner } from "../../../../types"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; type AddPartnerParams = { ecosystem: Ecosystem; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-delete-partner.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-delete-partner.ts index 019934a7c11..e97ba222931 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-delete-partner.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-delete-partner.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; -import type { Ecosystem } from "../../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; type DeletePartnerParams = { ecosystem: Ecosystem; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-ecosystem.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-ecosystem.ts index 2878eb42d6b..186f6d513e3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-ecosystem.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-ecosystem.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; -import type { Ecosystem } from "../../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; export function useUpdateEcosystem( params: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts index 1e78500af1f..2973213dafe 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/hooks/use-update-partner.ts @@ -3,7 +3,7 @@ import { useMutation, useQueryClient, } from "@tanstack/react-query"; -import type { Ecosystem, Partner } from "../../../../types"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; type UpdatePartnerParams = { partnerId: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx index 27d1ca9cb74..5d95622522d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/partners/[partner_id]/edit/page.tsx @@ -1,10 +1,10 @@ import { notFound } from "next/navigation"; +import { getAuthToken } from "@/api/auth-token"; +import { fetchEcosystem } from "@/api/ecosystems"; import { getTeamBySlug } from "@/api/team"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getAuthToken } from "../../../../../../../../../../../../../@/api/auth-token"; import { loginRedirect } from "../../../../../../../../../../../login/loginRedirect"; import { UpdatePartnerForm } from "../../../components/client/update-partner-form.client"; -import { fetchEcosystem } from "../../../hooks/fetchEcosystem"; import { fetchPartnerDetails } from "../../../hooks/fetchPartnerDetails"; export default async function EditPartnerPage({ @@ -36,11 +36,11 @@ export default async function EditPartnerPage({ }); try { - const ecosystem = await fetchEcosystem({ - authToken, - slug: ecosystemSlug, - teamIdOrSlug: teamSlug, - }); + const ecosystem = await fetchEcosystem(ecosystemSlug, teamSlug); + + if (!ecosystem) { + throw new Error("Ecosystem not found"); + } try { const partner = await fetchPartnerDetails({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts index f4f21bc3aa1..a53eec37ecc 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-ecosystem.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { apiServerProxy } from "@/actions/proxies"; -import type { Ecosystem } from "../../../types"; +import type { Ecosystem } from "@/api/ecosystems"; export function useEcosystem({ teamIdOrSlug, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts index d75b9721062..723d69b6c6e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/hooks/use-partners.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import type { Ecosystem, Partner } from "../../../types"; +import type { Ecosystem, Partner } from "@/api/ecosystems"; import { fetchPartners } from "../configuration/hooks/fetchPartners"; export function usePartners({ diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/actions/create-ecosystem.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/actions/create-ecosystem.ts index 43b4609cd86..f9b24629c5e 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/actions/create-ecosystem.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/create/actions/create-ecosystem.ts @@ -2,10 +2,10 @@ import "server-only"; import { redirect } from "next/navigation"; import { upload } from "thirdweb/storage"; +import { getAuthToken } from "@/api/auth-token"; import { BASE_URL } from "@/constants/env-utils"; import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getAuthToken } from "../../../../../../../../../@/api/auth-token"; export async function createEcosystem(options: { teamSlug: string; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx index a3858f9555d..e7b65f66286 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; import { getAuthToken } from "@/api/auth-token"; +import { fetchEcosystemList } from "@/api/ecosystems"; import { loginRedirect } from "../../../../../login/loginRedirect"; -import { fetchEcosystemList } from "./utils/fetchEcosystemList"; export default async function Page(props: { params: Promise<{ team_slug: string }>; @@ -15,12 +15,10 @@ export default async function Page(props: { loginRedirect(ecosystemLayoutPath); } - const ecosystems = await fetchEcosystemList(authToken, team_slug).catch( - (err) => { - console.error("failed to fetch ecosystems", err); - return []; - }, - ); + const ecosystems = await fetchEcosystemList(team_slug).catch((err) => { + console.error("failed to fetch ecosystems", err); + return []; + }); if (ecosystems[0]) { redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystem.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystem.ts deleted file mode 100644 index fc20152d2a8..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystem.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; -import type { Ecosystem } from "../types"; - -export async function fetchEcosystem( - slug: string, - authToken: string, - teamIdOrSlug: string, -) { - const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }, - ); - if (!res.ok) { - const data = await res.json(); - console.error(data); - return null; - } - - const data = (await res.json()) as { result: Ecosystem }; - return data.result; -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystemList.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystemList.ts deleted file mode 100644 index 94617169ff1..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/utils/fetchEcosystemList.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; -import type { Ecosystem } from "../types"; - -export async function fetchEcosystemList( - authToken: string, - teamIdOrSlug: string, -) { - const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`, - { - headers: { - Authorization: `Bearer ${authToken}`, - }, - }, - ); - - if (!res.ok) { - const data = await res.json(); - console.error(data); - throw new Error(data?.error?.message ?? "Failed to fetch ecosystems"); - } - - const data = (await res.json()) as { result: Ecosystem[] }; - return data.result; -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/_components/service-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/_components/service-card.tsx new file mode 100644 index 00000000000..fb85f10e96d --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/_components/service-card.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { EmptyChartState } from "@/components/analytics/empty-chart-state"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; + +type ServiceStatus = "active" | "pending" | "inactive"; + +type InfraServiceCardProps = { + title: string; + status: ServiceStatus; +}; + +export function InfraServiceCard({ title, status }: InfraServiceCardProps) { + return ( +
+ {/* Header row with status and optional action */} +
+
+

{title}

+ + {status === "active" + ? "Active" + : status === "pending" + ? "Pending" + : "Inactive"} + {status === "pending" && } + +
+
+ + +
+ ); +} + +// --- Helper Components --- + +function MetricPlaceholders({ + status, + serviceTitle, +}: { + status: ServiceStatus; + serviceTitle: string; +}) { + const metrics = getMetricsForService(serviceTitle); + + return ( +
+ {metrics.map((metric) => ( + + + {metric.label} + +
+ + {status === "active" ? ( + Coming Soon + ) : status === "pending" ? ( +

Activation in progress.

+ ) : ( +

Activate service to view metrics.

+ )} +
+
+
+ ))} +
+ ); +} + +type Metric = { key: string; label: string }; + +function getMetricsForService(title: string): Metric[] { + const normalized = title.toLowerCase(); + + if (normalized === "rpc") { + return [ + { key: "requests", label: "Requests" }, + { key: "monthly_active_developers", label: "Monthly Active Developers" }, + ]; + } + + if (normalized === "insight") { + return [ + { key: "requests", label: "Requests" }, + { key: "monthly_active_developers", label: "Monthly Active Developers" }, + ]; + } + + if (normalized === "account abstraction") { + return [ + { key: "transactions", label: "Transactions" }, + { key: "monthly_active_developers", label: "Monthly Active Developers" }, + { key: "gas_sponsored", label: "Gas Sponsored" }, + ]; + } + + // fallback empty + return []; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/page.tsx new file mode 100644 index 00000000000..9aa249e8a35 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/[chain_id]/page.tsx @@ -0,0 +1,178 @@ +import { InfoIcon } from "lucide-react"; +import { notFound, redirect } from "next/navigation"; +import { getChainSubscriptionForChain } from "@/api/team-subscription"; +import { formatToDollars } from "@/components/billing/formatToDollars"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { ChainIconClient } from "@/icons/ChainIcon"; +import { ToolTipLabel } from "../../../../../../../../@/components/ui/tooltip"; +import { getChain } from "../../../../../../(dashboard)/(chain)/utils"; +import { InfraServiceCard } from "./_components/service-card"; + +const PRODUCTS = [ + { + sku: "chain:infra:rpc", + title: "RPC", + }, + { + sku: "chain:infra:insight", + title: "Insight", + }, + { + sku: "chain:infra:account_abstraction", + title: "Account Abstraction", + }, +] as const; + +export default async function DeployInfrastructureOnChainPage(props: { + params: Promise<{ chain_id: string; team_slug: string }>; +}) { + const params = await props.params; + const chain = await getChain(params.chain_id); + + if (!chain) { + notFound(); + } + if (chain.slug !== params.chain_id) { + // redirect to the slug version of the page + redirect(`/team/${params.team_slug}/~/infrastructure/${chain.slug}`); + } + + const chainSubscription = await getChainSubscriptionForChain( + params.team_slug, + chain.chainId, + ); + + if (!chainSubscription) { + notFound(); + } + + const client = getClientThirdwebClient(); + + // Format renewal date and amount due for the subscription summary section + const renewalDate = new Date(chainSubscription.currentPeriodEnd); + const formattedRenewalDate = renewalDate.toLocaleDateString(undefined, { + day: "numeric", + month: "long", + year: "numeric", + }); + + // upcomingInvoice.amount is stored in cents – format to dollars if available + const formattedAmountDue = + chainSubscription.upcomingInvoice.amount !== null + ? formatToDollars(chainSubscription.upcomingInvoice.amount) + : "N/A"; + + return ( +
+ {/* Chain header */} +
+

+ Infrastructure for +

+ + + + {chain.icon && ( + + )} + {cleanChainName(chain.name)} + + + + Chain ID + {chain.chainId} + + + +
+ + {PRODUCTS.map((product) => { + const hasSku = chainSubscription.skus.includes(product.sku); + + // Map sku to chain service key + const skuToServiceKey: Record = { + "chain:infra:account_abstraction": "account-abstraction", + "chain:infra:insight": "insight", + "chain:infra:rpc": "rpc-edge", + }; + + const serviceKey = skuToServiceKey[product.sku]; + const chainService = chain.services.find( + (s) => s.service === serviceKey, + ); + const serviceEnabled = + chainService?.enabled ?? chainService?.status === "enabled"; + + let status: "active" | "pending" | "inactive"; + if (hasSku && serviceEnabled) { + status = "active"; + } else if (hasSku && !serviceEnabled) { + status = "pending"; + } else { + status = "inactive"; + } + + return ( + + ); + })} + + + {/* Subscription summary */} + + + {/* Left: header + info */} +
+
+

Subscription details

+ {chainSubscription.isLegacy && ( + + Enterprise + + This subscription is part of an enterprise agreement and + cannot be modified through the dashboard. Please contact + your account executive for any modifications. + + } + > + + + + )} +
+ +
+
+ Renews on + {formattedRenewalDate} +
+ +
+ Amount due + {formattedAmountDue} +
+
+
+
+
+
+ ); +} + +function cleanChainName(chainName: string) { + return chainName.replace("Mainnet", ""); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/[chain_id]/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/[chain_id]/page.tsx new file mode 100644 index 00000000000..9960caad2f2 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/[chain_id]/page.tsx @@ -0,0 +1,95 @@ +import { ArrowUpDownIcon } from "lucide-react"; +import Link from "next/link"; +import { notFound, redirect } from "next/navigation"; +import { getMembers } from "@/api/team-members"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { ChainIconClient } from "@/icons/ChainIcon"; +import { getChain } from "../../../../../../../(dashboard)/(chain)/utils"; +import { getValidAccount } from "../../../../../../../account/settings/getAccount"; +import { DeployInfrastructureForm } from "../_components/deploy-infrastructure-form.client"; + +export default async function DeployInfrastructureOnChainPage(props: { + params: Promise<{ chain_id: string; team_slug: string }>; +}) { + const params = await props.params; + + const pagePath = `/team/${params.team_slug}/~/infrastructure/deploy/${params.chain_id}`; + + const [account, chain, members] = await Promise.all([ + getValidAccount(pagePath), + getChain(params.chain_id), + getMembers(params.team_slug), + ]); + + if (!chain) { + notFound(); + } + if (chain.slug !== params.chain_id) { + // redirect to the slug version of the page + redirect(`/team/${params.team_slug}/~/infrastructure/deploy/${chain.slug}`); + } + + if (!members) { + notFound(); + } + + const accountMemberInfo = members.find((m) => m.accountId === account.id); + + if (!accountMemberInfo) { + notFound(); + } + + const client = getClientThirdwebClient(); + + return ( +
+
+

+ Deploy Infrastructure on +

+ + + + {chain.icon && ( + + )} + {cleanChainName(chain.name)} + + + + Chain ID + {chain.chainId} + + + + +
+ +
+ ); +} + +function cleanChainName(chainName: string) { + return chainName.replace("Mainnet", ""); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/_components/deploy-infrastructure-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/_components/deploy-infrastructure-form.client.tsx new file mode 100644 index 00000000000..1fe930bb1f2 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/_components/deploy-infrastructure-form.client.tsx @@ -0,0 +1,459 @@ +"use client"; + +import { useQueryState } from "nuqs"; +import { useMemo, useTransition } from "react"; +import { toast } from "sonner"; +import { getChainInfraCheckoutURL } from "@/actions/billing"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { InsightIcon } from "@/icons/InsightIcon"; +import { RPCIcon } from "@/icons/RPCIcon"; +import { SmartAccountIcon } from "@/icons/SmartAccountIcon"; +import { cn } from "@/lib/utils"; +import type { ChainInfraSKU } from "@/types/billing"; +import type { ChainMetadataWithServices } from "@/types/chain"; +import { searchParams } from "../search-params"; + +// Pricing constants (USD) +const SERVICE_CONFIG = { + accountAbstraction: { + annualPrice: 6120, + description: + "Let developers offer gasless transactions and programmable smart accounts out-of-the-box. Powered by ERC-4337 & ERC-7702 for wallet-less onboarding and custom account logic.", + icon: "SmartAccountIcon", + label: "Account Abstraction", + monthlyPrice: 600, + required: false, + sku: "chain:infra:account_abstraction" as const, + }, + insight: { + annualPrice: 15300, + description: + "Arm developers with real-time, indexed data via a turnkey REST API & Webhooks. Query any event, transaction, or token in milliseconds—no subgraph setup or indexer maintenance required.", + icon: "InsightIcon", + label: "Insight", + monthlyPrice: 1500, + required: false, + sku: "chain:infra:insight" as const, + }, + rpc: { + annualPrice: 15300, + description: + "Deliver blazing-fast, reliable RPC endpoints through our global edge network so developers enjoy low-latency reads & writes that seamlessly scale with their traffic.", + icon: "RPCIcon", + label: "RPC", + monthlyPrice: 1500, + required: true, + sku: "chain:infra:rpc" as const, + }, +} satisfies Record< + string, + { + label: string; + description: string; + sku: ChainInfraSKU; + monthlyPrice: number; + annualPrice: number; + required: boolean; + icon: "RPCIcon" | "InsightIcon" | "SmartAccountIcon"; + } +>; + +const formatUSD = (amount: number) => `$${amount.toLocaleString()}`; + +export function DeployInfrastructureForm(props: { + chain: ChainMetadataWithServices; + teamSlug: string; + isOwner: boolean; + className?: string; +}) { + const [isTransitionPending, startTransition] = useTransition(); + + const [frequency, setFrequency] = useQueryState( + "freq", + searchParams.freq.withOptions({ history: "replace", startTransition }), + ); + + const [addonsStr, setAddonsStr] = useQueryState( + "addons", + searchParams.addons.withOptions({ history: "replace", startTransition }), + ); + + const addons = useMemo(() => { + return addonsStr ? addonsStr.split(",").filter(Boolean) : []; + }, [addonsStr]); + + const includeInsight = addons.includes("insight"); + const includeAA = addons.includes("aa"); + + const selectedOrder = useMemo(() => { + const arr: (keyof typeof SERVICE_CONFIG)[] = ["rpc"]; + if (includeInsight) arr.push("insight"); + if (includeAA) arr.push("accountAbstraction"); + return arr; + }, [includeInsight, includeAA]); + + // NEW: count selected services and prepare bundle discount hint + const selectedCount = selectedOrder.length; + + const bundleHint = useMemo(() => { + if (selectedCount === 1) { + return "Add one more add-on to unlock a 10% bundle discount."; + } else if (selectedCount === 2) { + return "Add another add-on to increase your bundle discount to 15%."; + } else if (selectedCount >= 3) { + return "🎉 Congrats! You unlocked the maximum 15% bundle discount."; + } + return null; + }, [selectedCount]); + + const selectedServices = useMemo(() => { + return { + accountAbstraction: includeAA, + insight: includeInsight, + rpc: true, + } as const; + }, [includeInsight, includeAA]); + + const pricePerService = useMemo(() => { + const isAnnual = frequency === "annual"; + const mapping: Record = { + accountAbstraction: + SERVICE_CONFIG.accountAbstraction[ + isAnnual ? "annualPrice" : "monthlyPrice" + ], + insight: + SERVICE_CONFIG.insight[isAnnual ? "annualPrice" : "monthlyPrice"], + rpc: SERVICE_CONFIG.rpc[isAnnual ? "annualPrice" : "monthlyPrice"], + }; + return mapping; + }, [frequency]); + + // Calculate totals and savings correctly + const { subtotal, bundleDiscount, total, totalSavings, originalTotal } = + useMemo(() => { + let subtotal = 0; // price after annual discount but before bundle + let originalTotal = 0; // monthly price * months (12 if annual) with no discounts + let count = 0; + ( + Object.keys(selectedServices) as Array + ).forEach((key) => { + if (selectedServices[key]) { + subtotal += pricePerService[key]; + originalTotal += + SERVICE_CONFIG[key].monthlyPrice * + (frequency === "annual" ? 12 : 1); + count += 1; + } + }); + + let discountRate = 0; + if (count === 2) { + discountRate = 0.1; + } else if (count >= 3) { + discountRate = 0.15; + } + + const annualDiscount = + frequency === "annual" ? originalTotal - subtotal : 0; + const bundleDiscount = subtotal * discountRate; + const total = subtotal - bundleDiscount; + const totalSavings = annualDiscount + bundleDiscount; + return { + annualDiscount, + bundleDiscount, + originalTotal, + subtotal, + total, + totalSavings, + }; + }, [selectedServices, pricePerService, frequency]); + + const chainId = props.chain.chainId; + + const checkout = () => { + startTransition(async () => { + try { + const skus: ChainInfraSKU[] = [SERVICE_CONFIG.rpc.sku]; + if (includeInsight) skus.push(SERVICE_CONFIG.insight.sku); + if (includeAA) skus.push(SERVICE_CONFIG.accountAbstraction.sku); + + const res = await getChainInfraCheckoutURL({ + annual: frequency === "annual", + chainId, + skus, + teamSlug: props.teamSlug, + }); + + // If the action returns, it means redirect did not happen and we have an error + if (res.status === "error") { + toast.error(res.error); + } else if (res.status === "success") { + // replace the current page with the checkout page (which will then redirect back to us) + window.location.href = res.data; + } + } catch (err) { + console.error(err); + toast.error( + "Failed to create checkout session. Please try again later.", + ); + } + }); + }; + + const periodLabel = frequency === "annual" ? "/yr" : "/mo"; + const isAnnual = frequency === "annual"; + + return ( +
+ {/* Left column: service selection + frequency */} +
+

Select Services

+ + {/* Required service */} +
+ {}} + originalPrice={ + isAnnual ? SERVICE_CONFIG.rpc.monthlyPrice * 12 : undefined + } + periodLabel={periodLabel} + price={pricePerService.rpc} + required + selected + /> +
+ + {/* Optional add-ons */} +
+
+

Add-ons

+ {bundleHint && ( +

{bundleHint}

+ )} +
+
+ {/* Insight */} + { + const newVal = !includeInsight; + const newAddons = addons.filter((a) => a !== "insight"); + if (newVal) newAddons.push("insight"); + setAddonsStr(newAddons.join(",")); + }} + originalPrice={ + isAnnual ? SERVICE_CONFIG.insight.monthlyPrice * 12 : undefined + } + periodLabel={periodLabel} + price={pricePerService.insight} + selected={includeInsight} + /> + + {/* Account Abstraction */} + { + const newVal = !includeAA; + const newAddons = addons.filter((a) => a !== "aa"); + if (newVal) newAddons.push("aa"); + setAddonsStr(newAddons.join(",")); + }} + originalPrice={ + isAnnual + ? SERVICE_CONFIG.accountAbstraction.monthlyPrice * 12 + : undefined + } + periodLabel={periodLabel} + price={pricePerService.accountAbstraction} + selected={includeAA} + /> +
+
+
+ + {/* Right column: order summary */} +
+

Order Summary

+
+ {selectedOrder.map((key) => ( +
+ {SERVICE_CONFIG[key].label} + + {isAnnual && ( + + {formatUSD(SERVICE_CONFIG[key].monthlyPrice * 12)} + + )} + + {formatUSD(pricePerService[key])} + {periodLabel} + + +
+ ))} + +
+ Subtotal + + {formatUSD(subtotal)} + {periodLabel} + +
+ {bundleDiscount > 0 && ( +
+ + Bundle Discount ( + {Object.values(selectedServices).filter(Boolean).length === 2 + ? "10%" + : "15%"} + off) + + -{formatUSD(bundleDiscount)} +
+ )} + + {/* Billing Frequency Toggle */} +
+ Pay annually & save 15% + + setFrequency(checked ? "annual" : "monthly") + } + /> +
+ + {/* Total Row */} +
+ Total +

+ {totalSavings > 0 && ( + + {formatUSD(originalTotal)} + + )} + + {formatUSD(total)} {periodLabel} + +

+
+ + + {!props.isOwner && ( +

+ Only team owners can deploy infrastructure. +

+ )} +
+
+
+ ); +} + +// --- Service Card Component --- +type IconKey = "RPCIcon" | "InsightIcon" | "SmartAccountIcon"; + +function getIcon(icon: IconKey) { + switch (icon) { + case "RPCIcon": + return RPCIcon; + case "InsightIcon": + return InsightIcon; + case "SmartAccountIcon": + return SmartAccountIcon; + default: + return RPCIcon; + } +} + +function ServiceCard(props: { + label: string; + description: string; + price: number; + periodLabel: string; + originalPrice?: number; + selected?: boolean; + disabled?: boolean; + required?: boolean; + icon: IconKey; + onToggle: () => void; +}) { + const { + label, + description, + price, + periodLabel, + originalPrice, + selected, + disabled, + required, + icon, + onToggle, + } = props; + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/page.tsx new file mode 100644 index 00000000000..bcc7ddedc71 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/page.tsx @@ -0,0 +1,122 @@ +"use client"; + +/** + * This page lets customers select a chain to deploy infrastructure on as step one of a 2 step process + * in order to do this customers select a chain from the dropdown and then they can continue to `/team//~/infrastructure/deploy/[chain_id]` + */ + +import { ArrowRightIcon } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useState } from "react"; +import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; + +export default function DeployInfrastructurePage() { + const client = getClientThirdwebClient(); + + const [chainId, setChainId] = useState(undefined); + + const { team_slug } = useParams<{ team_slug: string }>(); + + return ( +
+
+

+ Deploy Infrastructure +

+
+
+ {/* Header */} +
+

Choose your Chain

+

+ Select the chain you'd like to deploy infrastructure on. In the next + step you'll pick which services you want to enable for all + developers on this chain. +

+
+ + {/* Chain selector */} +
+ + {/* Alternative paths hidden inside popover */} + + + + + +
    +
  1. + Option 1: Submit a PR to  + + ethereum-lists/chains + {" "} + to add your chain.{" "} + + (automatically added on PR merge) + +
  2. +
  3. + Option 2: Share your chain details via  + + this short form + + .
    + + (multiple days for your chain to be included) + +
  4. +
+
+
+
+ +
+ {chainId === undefined ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/search-params.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/search-params.ts new file mode 100644 index 00000000000..02dace3e9e6 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/deploy/search-params.ts @@ -0,0 +1,6 @@ +import { parseAsString, parseAsStringEnum } from "nuqs/server"; + +export const searchParams = { + addons: parseAsString.withDefault(""), + freq: parseAsStringEnum(["monthly", "annual"]).withDefault("monthly"), +}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/layout.tsx new file mode 100644 index 00000000000..06730684257 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/infrastructure/layout.tsx @@ -0,0 +1,16 @@ +import { redirect } from "next/navigation"; +import { getTeamBySlug } from "@/api/team"; + +export default async function Layout(props: { + children: React.ReactNode; + params: Promise<{ + team_slug: string; + }>; +}) { + const params = await props.params; + const team = await getTeamBySlug(params.team_slug); + if (!team) { + redirect("/team"); + } + return
{props.children}
; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/layout.tsx index 5766dc73a04..1904215e5c3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/layout.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { TabPathLinks } from "../../../../../../../@/components/ui/tabs"; +import { TabPathLinks } from "@/components/ui/tabs"; export default async function Layout(props: { children: React.ReactNode; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/sales/sales-settings.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/sales/sales-settings.tsx index 9115bcdd083..01ebb6d2bad 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/sales/sales-settings.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/nft/sales/sales-settings.tsx @@ -2,8 +2,8 @@ import type { UseFormReturn } from "react-hook-form"; import type { ThirdwebClient } from "thirdweb"; import { BasisPointsInput } from "@/components/blocks/BasisPointsInput"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { SolidityInput } from "@/components/solidity-inputs"; import { Form } from "@/components/ui/form"; -import { SolidityInput } from "../../../../../../../../../../@/components/solidity-inputs"; import { StepCard } from "../../_common/step-card"; import type { NFTSalesSettingsFormValues } from "../_common/form"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx index 52b1b1bd950..01435e37191 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/ftux.client.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import type { Project } from "@/api/projects"; import { type Step, StepsCard } from "@/components/blocks/StepsCard"; -import { Button } from "../../../../../../../../@/components/ui/button"; +import { Button } from "@/components/ui/button"; import { CreateVaultAccountButton } from "../../vault/components/create-vault-account.client"; import CreateServerWallet from "../server-wallets/components/create-server-wallet.client"; import type { Wallet } from "../server-wallets/wallet-table/types"; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx index 7c1a0342fd4..375d77fa65f 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/settings/components/index.tsx @@ -14,6 +14,7 @@ import { upload } from "thirdweb/storage"; import type { Project } from "@/api/projects"; import type { SMSCountryTiers } from "@/api/sms"; import type { Team } from "@/api/team"; +import { FileInput } from "@/components/blocks/FileInput"; import { GatedSwitch } from "@/components/blocks/GatedSwitch"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,6 @@ import { } from "@/schema/validations"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; import { toArrFromList } from "@/utils/string"; -import { FileInput } from "../../../../../../../../../@/components/blocks/FileInput"; import CountrySelector from "./sms-country-select/country-selector"; type InAppWalletSettingsPageProps = { diff --git a/packages/thirdweb/src/react/web/ui/components/Modal.tsx b/packages/thirdweb/src/react/web/ui/components/Modal.tsx index 9e12d87e1b3..c0093ac1843 100644 --- a/packages/thirdweb/src/react/web/ui/components/Modal.tsx +++ b/packages/thirdweb/src/react/web/ui/components/Modal.tsx @@ -47,8 +47,8 @@ export const Modal: React.FC<{ if (contentRef.current) { const animationConfig = { duration: modalCloseFadeOutDuration, - fill: "forwards", easing: "ease", + fill: "forwards", } as const; contentRef.current.animate([{ opacity: 0 }], { @@ -70,7 +70,7 @@ export const Modal: React.FC<{ }, [props.open]); return ( - + {/* Trigger */} {props.trigger && ( {props.trigger} @@ -86,12 +86,12 @@ export const Modal: React.FC<{ )} - + Connect Modal @@ -130,13 +130,13 @@ export const Modal: React.FC<{ {!props.hideCloseIcon && ( - + @@ -152,8 +152,8 @@ export const Modal: React.FC<{ export const CrossContainer = /* @__PURE__ */ StyledDiv({ position: "absolute", - top: spacing.lg, right: spacing.lg, + top: spacing.lg, transform: "translateX(6px)", }); @@ -183,39 +183,39 @@ const DialogContent = /* @__PURE__ */ StyledDiv((_) => { const theme = useCustomTheme(); return { - zIndex: 10000, - background: theme.colors.modalBg, "--bg": theme.colors.modalBg, - color: theme.colors.primaryText, - borderRadius: radius.lg, - position: "fixed", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: "calc(100vw - 40px)", - boxSizing: "border-box", + "& *": { + boxSizing: "border-box", + }, animation: `${modalAnimationDesktop} 300ms ease`, + background: theme.colors.modalBg, + border: `1px solid ${theme.colors.borderColor}`, + borderRadius: radius.lg, boxShadow: shadow.lg, + boxSizing: "border-box", + color: theme.colors.primaryText, + fontFamily: theme.fontFamily, + left: "50%", lineHeight: "normal", - border: `1px solid ${theme.colors.borderColor}`, outline: "none", overflow: "hidden", - fontFamily: theme.fontFamily, - "& *": { - boxSizing: "border-box", - }, + position: "fixed", + top: "50%", + transform: "translate(-50%, -50%)", + width: "calc(100vw - 40px)", + zIndex: 10000, [media.mobile]: { - top: "auto", + animation: `${modalAnimationMobile} 0.35s cubic-bezier(0.15, 1.15, 0.6, 1)`, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + borderRadius: radius.xl, bottom: 0, left: 0, + maxWidth: "none !important", right: 0, + top: "auto", transform: "none", width: "100vw", - animation: `${modalAnimationMobile} 0.35s cubic-bezier(0.15, 1.15, 0.6, 1)`, - borderRadius: radius.xl, - borderBottomRightRadius: 0, - borderBottomLeftRadius: 0, - maxWidth: "none !important", }, "& *::selection": { backgroundColor: theme.colors.selectedTextBg,