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:
MananTank
2024-09-10 18:32:04 +00:00
parent 25654aed5f
commit 75b9131dc8
8 changed files with 184 additions and 23 deletions
@@ -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;
}
@@ -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>
);
}
@@ -0,0 +1,3 @@
export function isOpChainId(chainId: number) {
return [10, 8453, 7777777, 252, 34443].includes(chainId);
}
@@ -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}
/>
)}
</>