diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index c2ea80074c4..cb1f07941fb 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -184,7 +184,7 @@ const cached_getInAppWalletUsage = unstable_cache( if (res?.status !== 200) { const reason = await res?.text(); console.error( - `Failed to fetch in-app wallet usage, ${res?.status} - ${res.statusText} - ${reason}`, + `Failed to fetch user wallets usage, ${res?.status} - ${res.statusText} - ${reason}`, ); return []; } 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 0054ac769cc..892d6ba1671 100644 --- a/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx +++ b/apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx @@ -38,11 +38,11 @@ type ShadcnSidebarBaseLink = { isActive?: (pathname: string) => boolean; }; -type ShadcnSidebarLink = +export type ShadcnSidebarLink = | ShadcnSidebarBaseLink | { group: string; - links: ShadcnSidebarBaseLink[]; + links: ShadcnSidebarLink[]; } | { separator: true; @@ -97,10 +97,7 @@ export function FullWidthSidebarLayout(props: { function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) { const activeLink = useActiveShadcnSidebarLink(props.links); - const parentSubNav = props.links.find( - (link) => - "subMenu" in link && link.links.some((l) => l.href === activeLink?.href), - ); + const parentSubNav = findParentSubmenu(props.links, activeLink?.href); return (
@@ -109,7 +106,7 @@ function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) { className="h-4 bg-muted-foreground/50" orientation="vertical" /> - {parentSubNav && "subMenu" in parentSubNav && ( + {parentSubNav && ( <> {parentSubNav.subMenu.label} @@ -131,24 +128,65 @@ function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) { return pathname?.startsWith(link.href); } - for (const link of links) { - if ("links" in link) { - for (const subLink of link.links) { - if (isActive(subLink)) { - return subLink; + function walk( + navLinks: ShadcnSidebarLink[], + ): ShadcnSidebarBaseLink | undefined { + for (const link of navLinks) { + if ("subMenu" in link) { + for (const subLink of link.links) { + if (isActive(subLink)) { + return subLink; + } + } + } else if ("href" in link) { + if (isActive(link)) { + return link; } } - } else if ("href" in link) { - if (isActive(link)) { - return link; + + if ("links" in link && !("subMenu" in link)) { + const nested = walk(link.links); + if (nested) { + return nested; + } } } + + return undefined; } + + return walk(links); }, [links, pathname]); return activeLink; } +function findParentSubmenu( + links: ShadcnSidebarLink[], + activeHref: string | undefined, +): Extract | undefined { + if (!activeHref) { + return undefined; + } + + for (const link of links) { + if ("subMenu" in link) { + if (link.links.some((subLink) => subLink.href === activeHref)) { + return link; + } + } + + if ("links" in link && !("subMenu" in link)) { + const nested = findParentSubmenu(link.links, activeHref); + if (nested) { + return nested; + } + } + } + + return undefined; +} + function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) { const pathname = usePathname(); diff --git a/apps/dashboard/src/@/components/blocks/project-page/project-page.tsx b/apps/dashboard/src/@/components/blocks/project-page/project-page.tsx index 1f7ffdb97ef..f72bf644ed0 100644 --- a/apps/dashboard/src/@/components/blocks/project-page/project-page.tsx +++ b/apps/dashboard/src/@/components/blocks/project-page/project-page.tsx @@ -12,10 +12,6 @@ import { type ProjectPageProps = { header: ProjectPageHeaderProps; footer?: ProjectPageFooterProps; - /** - * @deprecated only for legacy pages where we still need internal tabs for the moment, currently: - * - /webhooks - */ tabs?: TabPathLink[]; }; diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx index 280c8aa14e6..5637c26b107 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx @@ -337,7 +337,7 @@ export function InAppWalletUsersPageContent( isFetched={walletsQuery.isFetched} isPending={walletsQuery.isPending} tableContainerClassName="rounded-none border-x-0 border-b-0" - title="in-app wallets" + title="User wallets" />
diff --git a/apps/dashboard/src/@/components/misc/AnnouncementBanner.tsx b/apps/dashboard/src/@/components/misc/AnnouncementBanner.tsx index 5a13a15652b..a85501fbf75 100644 --- a/apps/dashboard/src/@/components/misc/AnnouncementBanner.tsx +++ b/apps/dashboard/src/@/components/misc/AnnouncementBanner.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { useLocalStorage } from "@/hooks/useLocalStorage"; -// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes function AnnouncementBannerUI(props: { href: string; label: string; @@ -45,12 +44,11 @@ function AnnouncementBannerUI(props: { } export function AnnouncementBanner() { - // return ( - // - // ); - return null; + return ( + + ); } diff --git a/apps/dashboard/src/@/constants/siwa-example-prompts.ts b/apps/dashboard/src/@/constants/siwa-example-prompts.ts index 711d0baaa55..4ef345afb41 100644 --- a/apps/dashboard/src/@/constants/siwa-example-prompts.ts +++ b/apps/dashboard/src/@/constants/siwa-example-prompts.ts @@ -1,5 +1,5 @@ export const siwaExamplePrompts = [ - "How do I add in-app wallet with sign in with google to my react app?", + "How do I add user wallet with sign in with google to my react app?", "How do I send a transaction in Unity?", "What does this contract revert error mean?", "I see thirdweb support id in my console log, can you help me?", diff --git a/apps/dashboard/src/@/storybook/stubs.ts b/apps/dashboard/src/@/storybook/stubs.ts index b05d6f85a37..75aff4de3fa 100644 --- a/apps/dashboard/src/@/storybook/stubs.ts +++ b/apps/dashboard/src/@/storybook/stubs.ts @@ -248,7 +248,7 @@ 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 User Wallets (Tier 1 at $0.00 / month)`, thirdwebSku: "usage:in_app_wallet", }, // AA Sponsorship diff --git a/apps/dashboard/src/@/utils/pricing.tsx b/apps/dashboard/src/@/utils/pricing.tsx index 3e9b8954619..1a653976968 100644 --- a/apps/dashboard/src/@/utils/pricing.tsx +++ b/apps/dashboard/src/@/utils/pricing.tsx @@ -23,7 +23,7 @@ export const TEAM_PLANS: Record< features: [ "Email Support", "48hr Guaranteed Response", - "Custom In-App Wallet Auth", + "Custom User Wallet Auth", ], price: 99, subTitle: "Everything in Starter, plus:", diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/Alerts.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/Alerts.tsx index 4e10dfefcc7..bcb1e1f1cde 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/Alerts.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/Alerts.tsx @@ -6,9 +6,9 @@ export const SmartWalletsBillingAlert = (props: { teamSlug: string }) => { return ( - Account Abstraction on Mainnet + Gas Sponsorship on Mainnet - To enable AA on mainnet chains,{" "} + To enable Gas Sponsorship on mainnet chains,{" "} subscribe to a billing plan. diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/index.tsx index 02ff236f64c..ed31590a56b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/index.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/factories/AccountFactories/index.tsx @@ -14,7 +14,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { UnderlineLink } from "@/components/ui/UnderlineLink"; export function DefaultFactoriesSection() { const data = [ @@ -38,13 +37,6 @@ export function DefaultFactoriesSection() {

Ready to use account factories that are pre-deployed on each chain.{" "} - - Learn how to use these in your apps -

diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx index 6ca23d1d256..b12081fe414 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/page.tsx @@ -1,163 +1,11 @@ -import type { Metadata } from "next"; -import { notFound, redirect } from "next/navigation"; -import type { SearchParams } from "nuqs/server"; -import { getUserOpUsage } from "@/api/analytics"; -import { getAuthToken } from "@/api/auth-token"; -import { getProject } from "@/api/project/projects"; -import { getTeamBySlug } from "@/api/team/get-team"; -import { - getLastNDaysRange, - type Range, -} from "@/components/analytics/date-range-selector"; -import { ProjectPage } from "@/components/blocks/project-page/project-page"; -import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { SmartAccountIcon } from "@/icons/SmartAccountIcon"; -import { getAbsoluteUrl } from "@/utils/vercel"; -import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary"; -import { SmartWalletsBillingAlert } from "./Alerts"; -import { AccountAbstractionAnalytics } from "./aa-analytics"; -import { searchParamLoader } from "./search-params"; - -interface PageParams { - team_slug: string; - project_slug: string; -} +import { redirect } from "next/navigation"; +// Redirect old Account Abstraction page to new Sponsored Gas Overview export default async function Page(props: { - params: Promise; - searchParams: Promise; - children: React.ReactNode; + params: Promise<{ team_slug: string; project_slug: string }>; }) { - const [params, searchParams, authToken] = await Promise.all([ - props.params, - searchParamLoader(props.searchParams), - getAuthToken(), - ]); - - if (!authToken) { - notFound(); - } - - const [team, project] = await Promise.all([ - getTeamBySlug(params.team_slug), - getProject(params.team_slug, params.project_slug), - ]); - - if (!team) { - redirect("/team"); - } - - if (!project) { - redirect(`/team/${params.team_slug}`); - } - - const interval = searchParams.interval ?? "week"; - const rangeType = searchParams.range || "last-120"; - - const range: Range = { - from: - rangeType === "custom" - ? searchParams.from - : getLastNDaysRange(rangeType).from, - to: - rangeType === "custom" - ? searchParams.to - : getLastNDaysRange(rangeType).to, - type: rangeType, - }; - - const userOpStats = await getUserOpUsage( - { - from: range.from, - period: interval, - projectId: project.id, - teamId: project.teamId, - to: range.to, - }, - authToken, - ); - - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: project.teamId, - }); - - const isBundlerServiceEnabled = !!project.services.find( - (s) => s.name === "bundler", - ); - - const hasSmartWalletsWithoutBilling = - isBundlerServiceEnabled && - team.billingStatus !== "validPayment" && - team.billingStatus !== "pastDue"; - - return ( - - {hasSmartWalletsWithoutBilling && ( - <> - -
- - )} -
- - - -
- + const params = await props.params; + redirect( + `/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas`, ); } - -const seo = { - desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.", - title: "The Complete Account Abstraction Toolkit | thirdweb", -}; - -export const metadata: Metadata = { - description: seo.desc, - openGraph: { - description: seo.desc, - images: [ - { - alt: seo.title, - height: 630, - url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`, - width: 1200, - }, - ], - title: seo.title, - }, - title: seo.title, -}; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/settings/SponsorshipPolicies/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/settings/SponsorshipPolicies/index.tsx index 80612ab72c2..72aa2e30aff 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/settings/SponsorshipPolicies/index.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/account-abstraction/settings/SponsorshipPolicies/index.tsx @@ -371,7 +371,7 @@ export function AccountAbstractionSettingsPage( of the{" "} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx index aff37b655fe..2c03fa4f88d 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx @@ -1,23 +1,22 @@ "use client"; import { - ArrowLeftRightIcon, BookTextIcon, BoxIcon, DatabaseIcon, HomeIcon, - LockIcon, RssIcon, Settings2Icon, WebhookIcon, } from "lucide-react"; -import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout"; -import { Badge } from "@/components/ui/badge"; +import { + FullWidthSidebarLayout, + type ShadcnSidebarLink, +} from "@/components/blocks/full-width-sidebar-layout"; import { BridgeIcon } from "@/icons/BridgeIcon"; import { ContractIcon } from "@/icons/ContractIcon"; import { InsightIcon } from "@/icons/InsightIcon"; import { NebulaIcon } from "@/icons/NebulaIcon"; import { PayIcon } from "@/icons/PayIcon"; -import { SmartAccountIcon } from "@/icons/SmartAccountIcon"; import { TokenIcon } from "@/icons/TokenIcon"; import { WalletProductIcon } from "@/icons/WalletProductIcon"; @@ -25,136 +24,136 @@ export function ProjectSidebarLayout(props: { layoutPath: string; children: React.ReactNode; }) { - return ( - - Transactions New - - ), - }, - { - href: `${props.layoutPath}/contracts`, - icon: ContractIcon, - label: "Contracts", - }, - { - href: `${props.layoutPath}/ai`, - icon: NebulaIcon, - label: "AI", - }, - ], - }, + const contentSidebarLinks = [ + { + exactMatch: true, + href: props.layoutPath, + icon: HomeIcon, + label: "Overview", + }, + { + separator: true, + }, + { + group: "Build", + links: [ { - separator: true, - }, - { - group: "Monetize", + subMenu: { + icon: WalletProductIcon, + label: "Wallets", + }, links: [ { - href: `${props.layoutPath}/payments`, - icon: PayIcon, - label: "Payments", + href: `${props.layoutPath}/wallets/user-wallets`, + label: "User Wallets", }, { - href: `${props.layoutPath}/bridge`, - icon: BridgeIcon, - label: "Bridge", + href: `${props.layoutPath}/wallets/server-wallets`, + label: "Server Wallets", }, { - href: `${props.layoutPath}/tokens`, - icon: TokenIcon, - label: "Tokens", + href: `${props.layoutPath}/wallets/sponsored-gas`, + label: "Gas Sponsorship", }, ], }, { - separator: true, + href: `${props.layoutPath}/contracts`, + icon: ContractIcon, + label: "Contracts", }, { - group: "Scale", - links: [ - { - href: `${props.layoutPath}/insight`, - icon: InsightIcon, - label: "Insight", - }, - { - href: `${props.layoutPath}/account-abstraction`, - icon: SmartAccountIcon, - label: "Account Abstraction", - }, - { - href: `${props.layoutPath}/rpc`, - icon: RssIcon, - label: "RPC", - }, - { - href: `${props.layoutPath}/vault`, - icon: LockIcon, - label: "Vault", - }, - // linkely want to move this to `team` level eventually - { - href: `${props.layoutPath}/engine`, - icon: DatabaseIcon, - label: "Engine", - }, - ], + href: `${props.layoutPath}/ai`, + icon: NebulaIcon, + label: "AI", }, - ]} - footerSidebarLinks={[ + ], + }, + { + separator: true, + }, + { + group: "Monetize", + links: [ { - separator: true, + href: `${props.layoutPath}/payments`, + icon: PayIcon, + label: "Payments", }, { - href: `${props.layoutPath}/webhooks/contracts`, - icon: WebhookIcon, - isActive: (pathname) => { - return pathname.startsWith(`${props.layoutPath}/webhooks`); - }, - label: "Webhooks", + href: `${props.layoutPath}/bridge`, + icon: BridgeIcon, + label: "Bridge", }, { - href: `${props.layoutPath}/settings`, - icon: Settings2Icon, - label: "Project Settings", + href: `${props.layoutPath}/tokens`, + icon: TokenIcon, + label: "Tokens", }, + ], + }, + { + separator: true, + }, + { + group: "Scale", + links: [ { - separator: true, + href: `${props.layoutPath}/insight`, + icon: InsightIcon, + label: "Insight", }, { - href: "https://portal.thirdweb.com", - icon: BookTextIcon, - label: "Documentation", + href: `${props.layoutPath}/rpc`, + icon: RssIcon, + label: "RPC", }, + // linkely want to move this to `team` level eventually { - href: "https://playground.thirdweb.com/connect/sign-in/button", - icon: BoxIcon, - label: "Playground", + href: `${props.layoutPath}/engine`, + icon: DatabaseIcon, + label: "Engine", }, - ]} + ], + }, + ] satisfies ShadcnSidebarLink[]; + + const footerSidebarLinks = [ + { + separator: true, + }, + { + href: `${props.layoutPath}/webhooks/contracts`, + icon: WebhookIcon, + isActive: (pathname) => { + return pathname.startsWith(`${props.layoutPath}/webhooks`); + }, + label: "Webhooks", + }, + { + href: `${props.layoutPath}/settings`, + icon: Settings2Icon, + label: "Project Settings", + }, + { + separator: true, + }, + { + href: "https://portal.thirdweb.com", + icon: BookTextIcon, + label: "Documentation", + }, + { + href: "https://playground.thirdweb.com/connect/sign-in/button", + icon: BoxIcon, + label: "Playground", + }, + ] satisfies ShadcnSidebarLink[]; + + return ( + {props.children} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx index aac56047d20..c0b05e81680 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx @@ -220,9 +220,9 @@ export function ProjectGeneralSettingsPageUI(props: { const projectLayout = `/team/${props.teamSlug}/${props.project.slug}`; const paths = { - aaConfig: `${projectLayout}/settings/account-abstraction`, + aaConfig: `${projectLayout}/wallets/sponsored-gas/configuration`, afterDeleteRedirectTo: `/team/${props.teamSlug}`, - inAppConfig: `${projectLayout}/settings/wallets`, + inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`, payConfig: `${projectLayout}/settings/payments`, }; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx deleted file mode 100644 index 8764b31bfed..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/account-abstraction/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { CircleAlertIcon } from "lucide-react"; -import { redirect } from "next/navigation"; -import { getAuthToken } from "@/api/auth-token"; -import { getProject } from "@/api/project/projects"; -import { getTeamBySlug } from "@/api/team/get-team"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { UnderlineLink } from "@/components/ui/UnderlineLink"; -import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; -import { getValidTeamPlan } from "@/utils/getValidTeamPlan"; -import { loginRedirect } from "@/utils/redirects"; -import { DefaultFactoriesSection } from "../../account-abstraction/factories/AccountFactories"; -import { YourFactoriesSection } from "../../account-abstraction/factories/AccountFactories/your-factories"; -import { AccountAbstractionSettingsPage } from "../../account-abstraction/settings/SponsorshipPolicies"; -import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb"; - -export default async function Page(props: { - params: Promise<{ team_slug: string; project_slug: string }>; -}) { - const { team_slug, project_slug } = await props.params; - - const [team, project, authToken] = await Promise.all([ - getTeamBySlug(team_slug), - getProject(team_slug, project_slug), - - getAuthToken(), - ]); - - if (!authToken) { - loginRedirect(`/team/${team_slug}/settings/account-abstraction`); - } - - if (!team) { - redirect("/team"); - } - - if (!project) { - redirect(`/team/${team_slug}`); - } - - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: team.id, - }); - - const bundlerService = project.services.find((s) => s.name === "bundler"); - - return ( -
- - -
- {!bundlerService ? ( - - - Account Abstraction service is disabled - - Enable Account Abstraction service in{" "} - - project settings - {" "} - to configure the sponsorship rules - - - ) : ( - <> - - - - - - )} -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/api/sms.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/api/sms.ts index 0ddb49eb77f..dd5b58188ef 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/api/sms.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/api/sms.ts @@ -1,6 +1,4 @@ import "server-only"; -import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; -import { API_SERVER_SECRET } from "@/constants/server-envs"; export type SMSCountryTiers = { tier1: string[]; @@ -10,52 +8,3 @@ export type SMSCountryTiers = { tier5: string[]; tier6: string[]; }; - -export async function getSMSCountryTiers() { - if (!API_SERVER_SECRET) { - throw new Error("API_SERVER_SECRET is not set"); - } - const res = await fetch( - `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/sms/list-country-tiers`, - { - headers: { - "Content-Type": "application/json", - "x-service-api-key": API_SERVER_SECRET, - }, - next: { - revalidate: 15 * 60, //15 minutes - }, - }, - ); - - if (!res.ok) { - console.error( - "Failed to fetch sms country tiers", - res.status, - res.statusText, - ); - res.body?.cancel(); - return { - tier1: [], - tier2: [], - tier3: [], - tier4: [], - tier5: [], - tier6: [], - }; - } - - try { - return (await res.json()).data as SMSCountryTiers; - } catch (e) { - console.error("Failed to parse sms country tiers", e); - return { - tier1: [], - tier2: [], - tier3: [], - tier4: [], - tier5: [], - tier6: [], - }; - } -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/components/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/components/index.tsx index 9ce0fb6b566..eb6aeefd517 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/components/index.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/wallets/components/index.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import type { ProjectEmbeddedWalletsService } from "@thirdweb-dev/service-utils"; -import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react"; +import { PlusIcon, Trash2Icon } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { type UseFormReturn, useFieldArray, useForm } from "react-hook-form"; @@ -13,7 +13,6 @@ import type { Project } from "@/api/project/projects"; import type { Team } from "@/api/team/get-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"; import { DynamicHeight } from "@/components/ui/DynamicHeight"; import { @@ -31,7 +30,6 @@ import { Spinner } from "@/components/ui/Spinner"; import { Textarea } from "@/components/ui/textarea"; import { UnderlineLink } from "@/components/ui/UnderlineLink"; import { planToTierRecordForGating } from "@/constants/planToTierRecord"; -import { updateProjectClient } from "@/hooks/useApi"; import { cn } from "@/lib/utils"; import { type ApiKeyEmbeddedWalletsValidationSchema, @@ -57,82 +55,6 @@ type UpdateAPIKeyTrackingData = { hasCustomAuthEndpoint: boolean; }; -export function InAppWalletSettingsPage(props: InAppWalletSettingsPageProps) { - const updateProject = useMutation({ - mutationFn: async (projectValues: Partial) => { - await updateProjectClient( - { - projectId: props.project.id, - teamId: props.teamId, - }, - projectValues, - ); - }, - }); - - function handleUpdateProject(projectValues: Partial) { - updateProject.mutate(projectValues, { - onError: (err) => { - toast.error("Failed to update an API Key"); - console.error(err); - }, - onSuccess: () => { - toast.success("In-App Wallet API Key configuration updated"); - }, - }); - } - - return ( - - ); -} - -const InAppWalletSettingsPageUI: React.FC< - InAppWalletSettingsPageProps & { - updateApiKey: ( - projectValues: Partial, - trackingData: UpdateAPIKeyTrackingData, - ) => void; - isUpdating: boolean; - smsCountryTiers: SMSCountryTiers; - } -> = (props) => { - const embeddedWalletService = props.project.services.find( - (service) => service.name === "embeddedWallets", - ); - - if (!embeddedWalletService) { - return ( - - - In-App wallets service is disabled - - Enable In-App wallets service in the{" "} - - project settings - {" "} - to configure settings - - - ); - } - - return ( - - ); -}; - export const InAppWalletSettingsUI: React.FC< InAppWalletSettingsPageProps & { updateApiKey: ( @@ -191,7 +113,7 @@ export const InAppWalletSettingsUI: React.FC< ) { return toast.error("Custom JSON Web Token configuration is invalid", { description: - "To use In-App Wallets with Custom JSON Web Token, provide JWKS URI and AUD.", + "To use User Wallets with Custom JSON Web Token, provide JWKS URI and AUD.", dismissible: true, duration: 9000, }); @@ -202,7 +124,7 @@ export const InAppWalletSettingsUI: React.FC< "Custom Authentication Endpoint configuration is invalid", { description: - "To use In-App Wallets with Custom Authentication Endpoint, provide a valid URL.", + "To use User Wallets with Custom Authentication Endpoint, provide a valid URL.", dismissible: true, duration: 9000, }, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx index 1bacd8bddd0..419bc8ae1ea 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx @@ -184,7 +184,7 @@ function EmptyChartContent(props: { +
+ } + > + + +
+ + + +
+ + + + + + ); +}; + +function BrandingFieldset(props: { + form: UseFormReturn; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; + client: ThirdwebClient; + isUpdating: boolean; +}) { + return ( + + +
+ } + > +
+ + props.form.setValue( + "branding", + checked + ? { + applicationImageUrl: "", + applicationName: "", + } + : undefined, + ), + }} + teamSlug={props.teamSlug} + trackingLabel="customEmailLogoAndName" + /> +
+ + +
+ ( + +
+ Application Image URL + + Logo that will display in the emails sent to users.{" "} +
The image must be squared + with recommended size of 72x72 px. +
+ + +
+ + + { + props.form.setValue("branding.applicationImageUrl", uri, { + shouldDirty: true, + shouldTouch: true, + }); + }} + uri={props.form.watch("branding.applicationImageUrl")} + /> + +
+ )} + /> + + {/* Application Name */} + ( + + Application Name + + Name that will be displayed in the emails sent to users.{" "} +
Defaults to your API Key's + name. +
+ + + + +
+ )} + /> +
+
+ + ); +} + +function AppImageFormControl(props: { + uri: string | undefined; + setUri: (uri: string) => void; + client: ThirdwebClient; +}) { + const [image, setImage] = useState(); + const resolveUrl = resolveSchemeWithErrorHandler({ + client: props.client, + uri: props.uri || undefined, + }); + + const uploadImage = useMutation({ + mutationFn: async (file: File) => { + const uri = await upload({ + client: props.client, + files: [file], + }); + + return uri; + }, + }); + + return ( +
+
+ { + try { + setImage(v); + const uri = await uploadImage.mutateAsync(v); + props.setUri(uri); + } catch (error) { + setImage(undefined); + toast.error("Failed to upload image", { + description: error instanceof Error ? error.message : undefined, + }); + } + }} + value={image || resolveUrl} + /> + + {uploadImage.isPending && ( +
+ +
+ )} +
+
+ ); +} + +function SMSCountryFields(props: { + form: UseFormReturn; + smsCountryTiers: SMSCountryTiers; + teamPlan: Team["billingPlan"]; + requiredPlan: Team["billingPlan"]; + teamSlug: string; +}) { + return ( +
+ + + props.form.setValue( + "smsEnabledCountryISOs", + checked ? ["US", "CA"] : [], + ), + }} + teamSlug={props.teamSlug} + trackingLabel="sms" + /> + + + + ( + + )} + /> + +
+ ); +} + +function JSONWebTokenFields(props: { + form: UseFormReturn; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; +}) { + return ( +
+ + Optionally allow users to authenticate with a custom JWT.{" "} + + Learn more + + + } + switchId="authentication-switch" + title="Custom JSON Web Token" + > + { + props.form.setValue( + "customAuthentication", + checked ? { aud: "", jwksUri: "" } : undefined, + ); + }, + }} + teamSlug={props.teamSlug} + trackingLabel="customAuthJWT" + /> + + + + ( + + JWKS URI + + + + Enter the URI of the JWKS + + + )} + /> + + ( + + AUD Value + + + + + Enter the audience claim for the JWT + + + + )} + /> + +
+ ); +} + +function AuthEndpointFields(props: { + form: UseFormReturn; + teamPlan: Team["billingPlan"]; + teamSlug: string; + requiredPlan: Team["billingPlan"]; +}) { + const expandCustomAuthEndpointField = + props.form.watch("customAuthEndpoint") !== undefined; + + return ( +
+ + Optionally allow users to authenticate with any arbitrary payload + that you provide.{" "} + + Learn more + + + } + switchId="auth-endpoint-switch" + title="Custom Authentication Endpoint" + > + { + props.form.setValue( + "customAuthEndpoint", + checked + ? { + authEndpoint: "", + customHeaders: [], + } + : undefined, + ); + }, + }} + teamSlug={props.teamSlug} + trackingLabel="customAuthEndpoint" + /> + + + {/* useFieldArray used on this component - it creates empty customAuthEndpoint.customHeaders array on mount */} + {/* So only mount if expandCustomAuthEndpointField is true */} + {expandCustomAuthEndpointField && ( + + )} +
+ ); +} + +function AuthEndpointFieldsContent(props: { + form: UseFormReturn; +}) { + const customHeaderFields = useFieldArray({ + control: props.form.control, + name: "customAuthEndpoint.customHeaders", + }); + + return ( +
+ ( + + Authentication Endpoint + + + + + Enter the URL of your server where we will send the user payload + for verification + + + + )} + /> + +
+ +
+ {customHeaderFields.fields.map((field, customHeaderIdx) => { + return ( +
+ + + +
+ ); + })} + + +
+ +

+ Set custom headers to be sent along the request with the payload to + the authentication endpoint above. This can be used to verify the + incoming requests +

+
+
+ ); +} + +function NativeAppsFieldset(props: { + form: UseFormReturn; + isUpdating: boolean; +}) { + const { form } = props; + return ( +
+ +
+ } + > + ( + + Allowed redirect URIs + +