Add Account Abstraction pages in teams (#4510)
## Problem solved
Short description of the bug fixed or feature added
<!-- start pr-codex -->
---
## PR-Codex overview
This PR updates API key handling and smart wallet components.
### Detailed summary
- Added `isOpChainId` function for chain validation
- Modified `getAuthToken` and `getAPIKey` functions
- Updated `SponsorshipPolicies` and `SmartWallets` components
- Introduced `AccountAbstractionPage` for account abstraction details
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { getAuthToken } from "./getAuthToken";
|
||||
|
||||
export async function getAPIKey(apiKeyId: string) {
|
||||
const authToken = getAuthToken();
|
||||
const apiServerURL = new URL(
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
|
||||
);
|
||||
|
||||
apiServerURL.pathname = `/v1/keys/${apiKeyId}`;
|
||||
|
||||
const res = await fetch(apiServerURL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
console.error(json.error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return json.data as ApiKey;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export function getAuthToken() {
|
||||
const cookiesManager = cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
|
||||
return token;
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
"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 ApiKeyService,
|
||||
useAccount,
|
||||
} from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { SmartWallets } from "../../../../../../components/smart-wallets";
|
||||
import { AAFooterSection } from "./AAFooterSection";
|
||||
import { isOpChainId } from "./isOpChain";
|
||||
|
||||
const TRACKING_CATEGORY = "smart-wallet";
|
||||
|
||||
export function AccountAbstractionPage(props: {
|
||||
apiKeyServices: ApiKeyService[];
|
||||
}) {
|
||||
const { apiKeyServices } = props;
|
||||
const looggedInUserQuery = useLoggedInUser();
|
||||
const accountQuery = useAccount();
|
||||
const chain = useActiveWalletChain();
|
||||
|
||||
const hasSmartWalletsWithoutBilling = useMemo(() => {
|
||||
if (!accountQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return apiKeyServices.find(
|
||||
(s) =>
|
||||
accountQuery.data.status !== AccountStatus.ValidPayment &&
|
||||
s.name === "bundler",
|
||||
);
|
||||
}, [apiKeyServices, accountQuery.data]);
|
||||
|
||||
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
|
||||
|
||||
return (
|
||||
<div className="max-sm:pt-6 pb-10">
|
||||
<h1 className="text-2xl lg:text-3xl font-semibold tracking-tight mb-1">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
|
||||
into your apps.{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
label="docs-wallets"
|
||||
category={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
View Documentation
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
{looggedInUserQuery.isLoading ? (
|
||||
<div className="flex items-center justify-center h-[400px] border border-border rounded-lg">
|
||||
<Spinner className="size-14" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
{hasSmartWalletsWithoutBilling ? (
|
||||
<SmartWalletsBillingAlert />
|
||||
) : (
|
||||
isOpChain && (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>
|
||||
Using the gas credits for OP chain paymaster
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Credits will automatically be applied to cover gas fees for
|
||||
any onchain activity across thirdweb services. <br />
|
||||
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
)}
|
||||
|
||||
<SmartWallets
|
||||
apiKeyServices={apiKeyServices}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-14" />
|
||||
<AAFooterSection trackingCategory={TRACKING_CATEGORY} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export function isOpChainId(chainId: number) {
|
||||
return [10, 8453, 7777777, 252, 34443].includes(chainId);
|
||||
}
|
||||
+26
-6
@@ -1,9 +1,29 @@
|
||||
export default function Page() {
|
||||
import { getProject } from "@/api/projects";
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAPIKey } from "../../../../../api/lib/getAPIKeys";
|
||||
import { AccountAbstractionPage } from "./AccountAbstractionPage";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: { team_slug: string; project_slug: string };
|
||||
}) {
|
||||
const { team_slug, project_slug } = props.params;
|
||||
const project = await getProject(team_slug, project_slug);
|
||||
|
||||
if (!project) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// THIS IS A WORKAROUND - project does not have `services` info - so we fetch APIKey object.
|
||||
const apiKey = await getAPIKey(project.id);
|
||||
|
||||
if (!apiKey) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full py-6 container flex items-center justify-center">
|
||||
<h1 className="text-4xl tracking-tighter text-muted-foreground">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
</div>
|
||||
<ChakraProviderSetup>
|
||||
<AccountAbstractionPage apiKeyServices={apiKey.services || []} />
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
type ApiKey,
|
||||
type ApiKeyService,
|
||||
type ApiKeyServicePolicy,
|
||||
type ApiKeyServicePolicyLimits,
|
||||
useAccount,
|
||||
@@ -43,7 +43,7 @@ import { z } from "zod";
|
||||
import { Spinner } from "../../../@/components/ui/Spinner/Spinner";
|
||||
|
||||
interface SponsorshipPoliciesProps {
|
||||
apiKey: ApiKey;
|
||||
apiKeyServices: ApiKeyService[];
|
||||
trackingCategory: string;
|
||||
}
|
||||
|
||||
@@ -97,10 +97,10 @@ const sponsorshipPoliciesValidationSchema = z.object({
|
||||
});
|
||||
|
||||
export const SponsorshipPolicies: React.FC<SponsorshipPoliciesProps> = ({
|
||||
apiKey,
|
||||
apiKeyServices,
|
||||
trackingCategory,
|
||||
}) => {
|
||||
const bundlerServiceId = apiKey.services?.find(
|
||||
const bundlerServiceId = apiKeyServices?.find(
|
||||
(s) => s.name === "bundler",
|
||||
)?.id;
|
||||
const { data: policy } = usePolicies(bundlerServiceId);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
"use state";
|
||||
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
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 { TabButtons } from "../../@/components/ui/tabs";
|
||||
import { AccountFactories } from "./AccountFactories";
|
||||
import { SponsorshipPolicies } from "./SponsorshipPolicies";
|
||||
|
||||
interface SmartWalletsProps {
|
||||
apiKey: ApiKey;
|
||||
apiKeyServices: ApiKeyService[];
|
||||
trackingCategory: string;
|
||||
defaultTabIndex?: number;
|
||||
}
|
||||
|
||||
export const SmartWallets: React.FC<SmartWalletsProps> = ({
|
||||
apiKey,
|
||||
apiKeyServices,
|
||||
trackingCategory,
|
||||
defaultTabIndex,
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const defaultTabIndex = Number.parseInt(searchParams?.get("tab") || "0");
|
||||
const [selectedTab, setSelectedTab] = useState<"factories" | "config">(
|
||||
defaultTabIndex === 1 ? "config" : "factories",
|
||||
);
|
||||
@@ -48,7 +49,7 @@ export const SmartWallets: React.FC<SmartWalletsProps> = ({
|
||||
|
||||
{selectedTab === "config" && (
|
||||
<SponsorshipPolicies
|
||||
apiKey={apiKey}
|
||||
apiKeyServices={apiKeyServices}
|
||||
trackingCategory={trackingCategory}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useActiveWalletChain } from "thirdweb/react";
|
||||
import {} from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
import { AAFooterSection } from "../../../app/team/[team_slug]/[project_slug]/connect/account-abstraction/AAFooterSection";
|
||||
import { isOpChainId } from "../../../app/team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain";
|
||||
|
||||
const TRACKING_CATEGORY = "smart-wallet";
|
||||
|
||||
@@ -34,7 +35,6 @@ export type SmartWalletFormData = {
|
||||
|
||||
const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
const router = useRouter();
|
||||
const defaultTabIndex = Number.parseInt(router.query.tab?.toString() || "0");
|
||||
const defaultClientId = router.query.clientId?.toString();
|
||||
const looggedInUserQuery = useLoggedInUser();
|
||||
const keysQuery = useApiKeys();
|
||||
@@ -78,9 +78,7 @@ const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
);
|
||||
}, [apiKeys, account]);
|
||||
|
||||
const isOpChain = useMemo(() => {
|
||||
return [10, 8453, 7777777, 252, 34443].includes(chain?.id || 0);
|
||||
}, [chain]);
|
||||
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
|
||||
|
||||
const seo = {
|
||||
title: "The Complete Account Abstraction Toolkit | thirdweb",
|
||||
@@ -170,9 +168,8 @@ const DashboardConnectAccountAbstraction: ThirdwebNextPage = () => {
|
||||
|
||||
{hasApiKeys && selectedKey && (
|
||||
<SmartWallets
|
||||
apiKey={selectedKey}
|
||||
apiKeyServices={selectedKey.services || []}
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
defaultTabIndex={defaultTabIndex}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user