diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/AccountAbstractionAPIKeysMenu.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/AccountAbstractionAPIKeysMenu.tsx new file mode 100644 index 00000000000..0b54fd92b64 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/AccountAbstractionAPIKeysMenu.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useDashboardRouter } from "@/lib/DashboardRouter"; +import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi"; +import { ApiKeysMenu } from "../../../../../components/settings/ApiKeys/Menu"; + +export function AccountAbstractionAPIKeysMenu(props: { + apiKeys: Pick[]; + selectedAPIKey: Pick; +}) { + const router = useDashboardRouter(); + return ( + { + router.push(`/dashboard/connect/account-abstraction/${key.key}`); + }} + /> + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/OpChainAlert.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/OpChainAlert.tsx new file mode 100644 index 00000000000..0ce6ac50896 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/OpChainAlert.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { CircleAlertIcon } from "lucide-react"; +import { useActiveWalletChain } from "thirdweb/react"; +import { isOpChainId } from "../../../../team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain"; + +export function OpChainAlert() { + const chain = useActiveWalletChain(); + const isOpChain = chain?.id ? isOpChainId(chain.id) : false; + + if (!isOpChain) { + return null; + } + + return ( + + + Using the gas credits for OP chain paymaster + + Credits will automatically be applied to cover gas fees for any onchain + activity across thirdweb services.
+ Eligible chains: OP Mainnet, Base, Zora, Frax, Mode. +
+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/PageHeader.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/PageHeader.tsx new file mode 100644 index 00000000000..b674d65e565 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/PageHeader.tsx @@ -0,0 +1,25 @@ +import { TrackedLinkTW } from "@/components/ui/tracked-link"; + +export function PageHeader() { + return ( +
+

+ Account Abstraction +

+ +

+ Easily integrate Account abstraction (ERC-4337) compliant smart accounts + into your apps.{" "} + + View Documentation + +

+
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx new file mode 100644 index 00000000000..7f07be4839e --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/[clientId]/page.tsx @@ -0,0 +1,80 @@ +import { AccountStatus } from "@3rdweb-sdk/react/hooks/useApi"; +import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts"; +import { ConnectSDKCard } from "components/shared/ConnectSDKCard"; +import { SmartWallets } from "components/smart-wallets"; +import { redirect } from "next/navigation"; +import { getAccount } from "../../../../../account/settings/getAccount"; +import { AccountAbstractionAPIKeysMenu } from "../AccountAbstractionAPIKeysMenu"; +import { OpChainAlert } from "../OpChainAlert"; +import { PageHeader } from "../PageHeader"; +import { getAASupportedAPIKeys } from "../getAASupportedAPIKeys"; + +export default async function Page(props: { + params: { + clientId: string; + }; + searchParams: { + tab?: string; + }; +}) { + const { clientId } = props.params; + const dashboardAccount = await getAccount(); + + if (!dashboardAccount) { + redirect( + `/login?next=${encodeURIComponent( + `/dashboard/connect/account-abstraction/${clientId}`, + )}`, + ); + } + + const apiKeys = await getAASupportedAPIKeys(); + const apiKey = apiKeys.find((key) => key.key === clientId); + + if (!apiKey) { + redirect("/dashboard/connect/account-abstraction"); + } + + const hasSmartWalletsWithoutBilling = apiKeys.find((k) => + k.services?.find( + (s) => + dashboardAccount.status !== AccountStatus.ValidPayment && + s.name === "bundler", + ), + ); + + return ( +
+
+ + +
+ ({ + name: x.name, + key: x.key, + }))} + selectedAPIKey={apiKey} + /> +
+
+ + {hasSmartWalletsWithoutBilling ? ( + + ) : ( + + )} + + + + +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/getAASupportedAPIKeys.ts b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/getAASupportedAPIKeys.ts new file mode 100644 index 00000000000..84456e2a53b --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/getAASupportedAPIKeys.ts @@ -0,0 +1,7 @@ +import { getApiKeys } from "../../../../api/lib/getAPIKeys"; + +export async function getAASupportedAPIKeys() { + return (await getApiKeys()).filter((key) => { + return !!(key.services || []).find((srv) => srv.name === "bundler"); + }); +} diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/layout.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/layout.tsx new file mode 100644 index 00000000000..26e8346e715 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/layout.tsx @@ -0,0 +1,29 @@ +import { ChakraProviderSetup } from "@/components/ChakraProviderSetup"; +import type { Metadata } from "next"; +import { getAbsoluteUrl } from "../../../../../lib/vercel-utils"; + +export default function Layout(props: { children: React.ReactNode }) { + return {props.children}; +} + +const seo = { + title: "The Complete Account Abstraction Toolkit | thirdweb", + desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.", +}; + +export const metadata: Metadata = { + title: seo.title, + description: seo.desc, + openGraph: { + title: seo.title, + description: seo.desc, + images: [ + { + url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`, + width: 1200, + height: 630, + alt: seo.title, + }, + ], + }, +}; diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/page.tsx new file mode 100644 index 00000000000..3f39d50bca6 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/dashboard/connect/account-abstraction/page.tsx @@ -0,0 +1,36 @@ +import { redirect } from "next/navigation"; +import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys"; +import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard"; +import { getAuthToken } from "../../../../api/lib/getAuthToken"; +import { PageHeader } from "./PageHeader"; +import { getAASupportedAPIKeys } from "./getAASupportedAPIKeys"; + +export default async function Page() { + const authToken = getAuthToken(); + + if (!authToken) { + redirect( + `/login?next=${encodeURIComponent("/dashboard/connect/account-abstraction")}`, + ); + } + + const apiKeys = await getAASupportedAPIKeys(); + const firstKey = apiKeys[0]; + + if (firstKey) { + redirect(`/dashboard/connect/account-abstraction/${firstKey.key}`); + } + + return ( +
+ +
+ +
+ +
+ ); +} diff --git a/apps/dashboard/src/components/settings/ApiKeys/Alerts.tsx b/apps/dashboard/src/components/settings/ApiKeys/Alerts.tsx index 2947c62a05c..7738d425d48 100644 --- a/apps/dashboard/src/components/settings/ApiKeys/Alerts.tsx +++ b/apps/dashboard/src/components/settings/ApiKeys/Alerts.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Alert, AlertDescription, diff --git a/apps/dashboard/src/components/smart-wallets/index.tsx b/apps/dashboard/src/components/smart-wallets/index.tsx index 4a2824973cc..ae05d4eb08b 100644 --- a/apps/dashboard/src/components/smart-wallets/index.tsx +++ b/apps/dashboard/src/components/smart-wallets/index.tsx @@ -1,8 +1,7 @@ -"use state"; +"use client"; import { TabButtons } from "@/components/ui/tabs"; import type { ApiKeyService } from "@3rdweb-sdk/react/hooks/useApi"; -import { useSearchParams } from "next/navigation"; import { useState } from "react"; import { AccountFactories } from "./AccountFactories"; import { AccountAbstractionSettingsPage } from "./SponsorshipPolicies"; @@ -10,16 +9,16 @@ import { AccountAbstractionSettingsPage } from "./SponsorshipPolicies"; interface SmartWalletsProps { apiKeyServices: ApiKeyService[]; trackingCategory: string; + defaultTab: 0 | 1; } export const SmartWallets: React.FC = ({ apiKeyServices, trackingCategory, + defaultTab, }) => { - const searchParams = useSearchParams(); - const defaultTabIndex = Number.parseInt(searchParams?.get("tab") || "1"); const [selectedTab, setSelectedTab] = useState<"factories" | "config">( - defaultTabIndex === 1 ? "config" : "factories", + defaultTab === 0 ? "config" : "factories", ); return ( diff --git a/apps/dashboard/src/hooks/useLocalStorage.ts b/apps/dashboard/src/hooks/useLocalStorage.ts index 8e7914943df..fb1414287b0 100644 --- a/apps/dashboard/src/hooks/useLocalStorage.ts +++ b/apps/dashboard/src/hooks/useLocalStorage.ts @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useState } from "react"; import { isBrowser } from "utils/isBrowser"; diff --git a/apps/dashboard/src/page-id.ts b/apps/dashboard/src/page-id.ts index dd488d90025..1ccf2098398 100644 --- a/apps/dashboard/src/page-id.ts +++ b/apps/dashboard/src/page-id.ts @@ -96,8 +96,6 @@ export enum PageId { // thirdweb.com/dashboard/infrastructure/storage DashboardSettingsStorage = "dashboard-storage", - // thirdweb.com/dashboard/connect/smart-wallet - DashboardConnectAccountAbstraction = "dashboard-wallets-smart-wallet", // thirdweb.com/dashboard/contracts/build DashboardContractsBuild = "dashboard-contracts-build", diff --git a/apps/dashboard/src/pages/dashboard/connect/account-abstraction.tsx b/apps/dashboard/src/pages/dashboard/connect/account-abstraction.tsx deleted file mode 100644 index c1ce6997eda..00000000000 --- a/apps/dashboard/src/pages/dashboard/connect/account-abstraction.tsx +++ /dev/null @@ -1,199 +0,0 @@ -"use client"; -import { Spinner } from "@/components/ui/Spinner/Spinner"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { TrackedLinkTW } from "@/components/ui/tracked-link"; -import { - AccountStatus, - type ApiKey, - useAccount, - useApiKeys, -} from "@3rdweb-sdk/react/hooks/useApi"; -import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser"; -import { AppLayout } from "components/app-layouts/app"; -import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts"; -import { ApiKeysMenu } from "components/settings/ApiKeys/Menu"; -import { NoApiKeys } from "components/settings/ApiKeys/NoApiKeys"; -import { ConnectSDKCard } from "components/shared/ConnectSDKCard"; -import { SmartWallets } from "components/smart-wallets"; -import { getAbsoluteUrl } from "lib/vercel-utils"; -import { CircleAlertIcon } from "lucide-react"; -import { NextSeo } from "next-seo"; -import { useRouter } from "next/router"; -import { PageId } from "page-id"; -import { useMemo, useState } from "react"; -import { useActiveWalletChain } from "thirdweb/react"; -import type { ThirdwebNextPage } from "utils/types"; -import { ConnectSidebarLayout } from "../../../app/(dashboard)/dashboard/connect/DashboardConnectLayout"; -import { isOpChainId } from "../../../app/team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain"; - -const TRACKING_CATEGORY = "smart-wallet"; - -export type SmartWalletFormData = { - chainAndFactoryAddress: string; - clientId: string; -}; - -const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => { - const router = useRouter(); - const defaultClientId = router.query.clientId?.toString(); - const looggedInUserQuery = useLoggedInUser(); - const keysQuery = useApiKeys(); - const [selectedKey_, setSelectedKey] = useState(); - const meQuery = useAccount(); - const account = meQuery?.data; - const chain = useActiveWalletChain(); - - const apiKeys = useMemo(() => { - return (keysQuery?.data || []).filter((key) => { - return !!(key.services || []).find((srv) => srv.name === "bundler"); - }); - }, [keysQuery]); - - const hasApiKeys = apiKeys.length > 0; - - // compute the actual selected key based on if there is a state, if there is a query param, or otherwise the first one - const selectedKey = useMemo(() => { - if (selectedKey_) { - return selectedKey_; - } - if (apiKeys.length) { - if (defaultClientId) { - return apiKeys.find((k) => k.key === defaultClientId); - } - return apiKeys[0]; - } - return undefined; - }, [apiKeys, defaultClientId, selectedKey_]); - - const hasSmartWalletsWithoutBilling = useMemo(() => { - if (!account || !apiKeys) { - return; - } - - return apiKeys.find((k) => - k.services?.find( - (s) => - account.status !== AccountStatus.ValidPayment && s.name === "bundler", - ), - ); - }, [apiKeys, account]); - - const isOpChain = chain?.id ? isOpChainId(chain.id) : false; - - const seo = { - title: "The Complete Account Abstraction Toolkit | thirdweb", - desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.", - }; - - const isPending = looggedInUserQuery.isPending || keysQuery.isPending; - - return ( -
- - -
-
-

- Account Abstraction -

- -

- Easily integrate Account abstraction (ERC-4337) compliant smart - accounts into your apps.{" "} - - View Documentation - -

-
- - {hasApiKeys && ( -
- {selectedKey && ( - - )} -
- )} -
- - {isPending ? ( -
- -
- ) : ( - <> - {hasSmartWalletsWithoutBilling ? ( - - ) : ( - isOpChain && ( - - - - Using the gas credits for OP chain paymaster - - - Credits will automatically be applied to cover gas fees for - any onchain activity across thirdweb services.
- Eligible chains: OP Mainnet, Base, Zora, Frax, Mode. -
-
- ) - )} - - {!hasApiKeys && } - - {hasApiKeys && selectedKey && ( - - )} - - )} - - -
- ); -}; - -DashboardConnectAccountAbstraction.getLayout = (page, props) => ( - - {page} - -); - -DashboardConnectAccountAbstraction.pageId = - PageId.DashboardConnectAccountAbstraction; - -export default DashboardConnectAccountAbstraction;