diff --git a/apps/playground-web/knip.json b/apps/playground-web/knip.json new file mode 100644 index 00000000000..2049978134b --- /dev/null +++ b/apps/playground-web/knip.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "next": true, + "ignore": ["src/components/ui/**", "src/app/insight/utils.ts"], + "project": ["src/**"], + "ignoreDependencies": ["server-only"], + "ignoreBinaries": ["biome"] +} diff --git a/apps/playground-web/package.json b/apps/playground-web/package.json index 9e9ff0e8070..16cf506335d 100644 --- a/apps/playground-web/package.json +++ b/apps/playground-web/package.json @@ -8,35 +8,30 @@ "start": "next start", "format": "biome format ./src --write", "prelint": "biome check ./src", - "lint": "eslint ./src", + "lint": "biome check ./src && knip && eslint ./src", "prefix": "biome check ./src --fix", "fix": "eslint ./src --fix", "typecheck": "tsc --noEmit", "update-insight-blueprints": "bun scripts/updateInsightBlueprints.ts" }, "dependencies": { - "@abstract-foundation/agw-client": "^1.6.2", "@abstract-foundation/agw-react": "^1.5.4", "@hookform/resolvers": "^3.9.1", - "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-checkbox": "^1.1.5", - "@radix-ui/react-dialog": "1.1.7", - "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", "@radix-ui/react-radio-group": "^1.2.4", - "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-switch": "^1.1.4", - "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "1.2.0", "@tanstack/react-query": "5.72.1", "@thirdweb-dev/engine": "^0.0.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "jose": "6.0.10", + "date-fns": "4.1.0", "lucide-react": "0.487.0", "next": "15.2.4", "next-themes": "^0.4.6", @@ -52,7 +47,6 @@ "shiki": "1.27.0", "tailwind-merge": "^2.6.0", "thirdweb": "workspace:*", - "timeago.js": "^4.0.2", "use-debounce": "^10.0.4", "zod": "3.24.2" }, @@ -60,9 +54,12 @@ "@types/node": "22.14.0", "@types/react": "19.1.0", "@types/react-dom": "19.1.1", + "autoprefixer": "^10.4.21", "eslint": "8.57.0", + "eslint-config-biome": "1.9.4", "eslint-config-next": "15.2.4", "eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405", + "knip": "5.47.0", "postcss": "8.5.3", "tailwindcss": "3.4.17", "tailwindcss-animate": "^1.0.7", diff --git a/apps/playground-web/postcss.config.js b/apps/playground-web/postcss.config.js new file mode 100644 index 00000000000..12a703d900d --- /dev/null +++ b/apps/playground-web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/playground-web/postcss.config.mjs b/apps/playground-web/postcss.config.mjs deleted file mode 100644 index 1a69fd2a450..00000000000 --- a/apps/playground-web/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/apps/playground-web/public/account-abstraction.png b/apps/playground-web/public/account-abstraction.png deleted file mode 100644 index de478e56046..00000000000 Binary files a/apps/playground-web/public/account-abstraction.png and /dev/null differ diff --git a/apps/playground-web/public/airdrop.avif b/apps/playground-web/public/airdrop.avif deleted file mode 100644 index 51492b89e12..00000000000 Binary files a/apps/playground-web/public/airdrop.avif and /dev/null differ diff --git a/apps/playground-web/public/auth.png b/apps/playground-web/public/auth.png deleted file mode 100644 index d97cbba3293..00000000000 Binary files a/apps/playground-web/public/auth.png and /dev/null differ diff --git a/apps/playground-web/public/blockchain-api.png b/apps/playground-web/public/blockchain-api.png deleted file mode 100644 index 45a28a2ad57..00000000000 Binary files a/apps/playground-web/public/blockchain-api.png and /dev/null differ diff --git a/apps/playground-web/public/connectors.png b/apps/playground-web/public/connectors.png deleted file mode 100644 index eddaac14583..00000000000 Binary files a/apps/playground-web/public/connectors.png and /dev/null differ diff --git a/apps/playground-web/public/headless-ui-header.png b/apps/playground-web/public/headless-ui-header.png deleted file mode 100644 index cf87071feea..00000000000 Binary files a/apps/playground-web/public/headless-ui-header.png and /dev/null differ diff --git a/apps/playground-web/public/in-app-wallet.png b/apps/playground-web/public/in-app-wallet.png deleted file mode 100644 index cc8933ebb9c..00000000000 Binary files a/apps/playground-web/public/in-app-wallet.png and /dev/null differ diff --git a/apps/playground-web/public/insight-hero.avif b/apps/playground-web/public/insight-hero.avif deleted file mode 100644 index 9e18ed76062..00000000000 Binary files a/apps/playground-web/public/insight-hero.avif and /dev/null differ diff --git a/apps/playground-web/public/pay.png b/apps/playground-web/public/pay.png deleted file mode 100644 index e0e47f9517c..00000000000 Binary files a/apps/playground-web/public/pay.png and /dev/null differ diff --git a/apps/playground-web/public/ub.png b/apps/playground-web/public/ub.png deleted file mode 100644 index 6448e1d2a10..00000000000 Binary files a/apps/playground-web/public/ub.png and /dev/null differ diff --git a/apps/playground-web/src/app/api/airdrop/route.ts b/apps/playground-web/src/app/api/airdrop/route.ts deleted file mode 100644 index bec24c4ac7b..00000000000 --- a/apps/playground-web/src/app/api/airdrop/route.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Engine } from "@thirdweb-dev/engine"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -const CHAIN_ID = "84532"; -const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; - -const engine = new Engine({ - url: process.env.ENGINE_URL as string, - accessToken: process.env.ENGINE_ACCESS_TOKEN as string, -}); - -interface MintResult { - queueId: string; - status: "Queued" | "Sent" | "Mined" | "error"; - transactionHash?: string; - blockExplorerUrl?: string; - errorMessage?: string; - toAddress: string; - amount: string; - chainId: number; - network: "Base Sep"; -} - -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - console.log("Request body:", body); - - const { contractAddress, data } = body; - if (!Array.isArray(data)) { - return NextResponse.json( - { error: "Invalid data format" }, - { status: 400 }, - ); - } - - if (data.length === 0) { - return NextResponse.json({ error: "Empty data array" }, { status: 400 }); - } - - console.log(`Attempting to mint batch to ${data.length} receivers`); - console.log("Using CONTRACT_ADDRESS:", contractAddress); - - const res = await engine.erc20.mintBatchTo( - CHAIN_ID, - contractAddress, - BACKEND_WALLET_ADDRESS, - { - data: data.map((item) => ({ - toAddress: item.toAddress, - amount: item.amount, - })), - }, - ); - - console.log("Mint batch initiated, queue ID:", res.result.queueId); - const result = await pollToMine(res.result.queueId, data[0]); - return NextResponse.json([result]); - } catch (error: unknown) { - console.error("Error minting ERC20 tokens", error); - return NextResponse.json( - [ - { - queueId: "", - status: "error", - errorMessage: - error instanceof Error - ? error.message - : "An unknown error occurred", - toAddress: "", - amount: "", - chainId: Number.parseInt(CHAIN_ID), - network: "Base Sep", - }, - ], - { status: 500 }, - ); - } -} - -async function pollToMine( - queueId: string, - firstItem: { toAddress: string; amount: string }, -): Promise { - let attempts = 0; - const maxAttempts = 10; - - while (attempts < maxAttempts) { - try { - const status = await engine.transaction.status(queueId); - - if (status.result.status === "mined") { - console.log( - "Transaction mined! 🥳 ERC20 tokens have been minted", - queueId, - ); - const transactionHash = status.result.transactionHash; - const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`; - console.log("View transaction on the blockexplorer:", blockExplorerUrl); - return { - queueId, - status: "Mined", - transactionHash: transactionHash ?? undefined, - blockExplorerUrl: blockExplorerUrl, - toAddress: firstItem.toAddress, - amount: firstItem.amount, - chainId: Number.parseInt(CHAIN_ID), - network: "Base Sep", - }; - } - - if (status.result.status === "errored") { - console.error("Mint failed", queueId); - console.error(status.result.errorMessage); - return { - queueId, - status: "error", - errorMessage: status.result.errorMessage ?? "Unknown error occurred", - toAddress: firstItem.toAddress, - amount: firstItem.amount, - chainId: Number.parseInt(CHAIN_ID), - network: "Base Sep", - }; - } - } catch (error) { - console.error("Error checking transaction status:", error); - } - - attempts++; - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - - return { - queueId, - status: "error", - errorMessage: "Transaction did not mine within the expected time", - toAddress: firstItem.toAddress, - amount: firstItem.amount, - chainId: Number.parseInt(CHAIN_ID), - network: "Base Sep", - }; -} diff --git a/apps/playground-web/src/app/api/claimTo/route.ts b/apps/playground-web/src/app/api/claimTo/route.ts deleted file mode 100644 index a732f87b56a..00000000000 --- a/apps/playground-web/src/app/api/claimTo/route.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Engine } from "@thirdweb-dev/engine"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -const BASESEP_CHAIN_ID = "84532"; -const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; - -const engine = new Engine({ - url: process.env.ENGINE_URL as string, - accessToken: process.env.ENGINE_ACCESS_TOKEN as string, -}); - -type TransactionStatus = "Queued" | "Sent" | "Mined" | "error"; - -interface ClaimResult { - queueId: string; - status: TransactionStatus; - transactionHash?: string | undefined | null; - blockExplorerUrl?: string | undefined | null; - errorMessage?: string; - toAddress?: string; - amount?: string; - chainId?: string; - timestamp?: number; -} - -// Store ongoing polling processes -const pollingProcesses = new Map(); - -// Helper function to make a single claim -async function makeClaimRequest( - chainId: string, - contractAddress: string, - data: { - recipient: string; - quantity: number; - }, -): Promise { - try { - // Validate the recipient address format - if (!data.recipient.match(/^0x[a-fA-F0-9]{40}$/)) { - throw new Error("Invalid wallet address format"); - } - - const res = await engine.erc721.claimTo( - chainId, - contractAddress, - BACKEND_WALLET_ADDRESS, - { - receiver: data.recipient.toString(), - quantity: data.quantity.toString(), - txOverrides: { - gas: "530000", - maxFeePerGas: "1000000000", - maxPriorityFeePerGas: "1000000000", - }, - }, - ); - - const initialResponse: ClaimResult = { - queueId: res.result.queueId, - status: "Queued", - toAddress: data.recipient, - amount: data.quantity.toString(), - chainId, - timestamp: Date.now(), - }; - - startPolling(res.result.queueId); - return initialResponse; - } catch (error) { - console.error("Claim request error:", error); - throw error; - } -} - -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - - if (!body.receiver || !body.quantity || !body.contractAddress) { - return NextResponse.json( - { error: "Missing receiver, quantity, or contract address" }, - { status: 400 }, - ); - } - - // Validate contract address format - if (!body.contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) { - return NextResponse.json( - { error: "Invalid contract address format" }, - { status: 400 }, - ); - } - - const result = await makeClaimRequest( - BASESEP_CHAIN_ID, - body.contractAddress, - { - recipient: body.receiver, - quantity: Number.parseInt(body.quantity), - }, - ); - - return NextResponse.json({ result }); - } catch (error) { - console.error("API Error:", error); - return NextResponse.json( - { - error: - error instanceof Error ? error.message : "Unknown error occurred", - details: error instanceof Error ? error.stack : undefined, - }, - { status: 400 }, - ); - } -} - -function startPolling(queueId: string) { - const maxPollingTime = 5 * 60 * 1000; // 5 minutes timeout - const startTime = Date.now(); - - const pollingInterval = setInterval(async () => { - try { - // Check if we've exceeded the maximum polling time - if (Date.now() - startTime > maxPollingTime) { - clearInterval(pollingInterval); - pollingProcesses.delete(queueId); - console.log(`Polling timeout for queue ID: ${queueId}`); - return; - } - - const result = await pollToMine(queueId); - if (result.status === "Mined" || result.status === "error") { - clearInterval(pollingInterval); - pollingProcesses.delete(queueId); - console.log("Final result:", result); - } - } catch (error) { - console.error("Error in polling process:", error); - clearInterval(pollingInterval); - pollingProcesses.delete(queueId); - } - }, 1500); - - pollingProcesses.set(queueId, pollingInterval); -} - -async function pollToMine(queueId: string): Promise { - console.log(`Polling for queue ID: ${queueId}`); - const status = await engine.transaction.status(queueId); - console.log(`Current status: ${status.result.status}`); - - switch (status.result.status) { - case "queued": - console.log("Transaction is queued"); - return { queueId, status: "Queued" }; - case "sent": - console.log("Transaction is submitted to the network"); - return { queueId, status: "Sent" }; - case "mined": { - console.log( - "Transaction mined! 🥳 ERC721 token has been claimed", - queueId, - ); - const transactionHash = status.result.transactionHash; - const blockExplorerUrl = - status.result.chainId === BASESEP_CHAIN_ID - ? `https://base-sepolia.blockscout.com/tx/${transactionHash}` - : ""; - console.log("View transaction on the blockexplorer:", blockExplorerUrl); - return { - queueId, - status: "Mined", - transactionHash: transactionHash ?? undefined, - blockExplorerUrl: blockExplorerUrl, - }; - } - case "errored": - console.error("Claim failed", queueId); - console.error(status.result.errorMessage); - return { - queueId, - status: "error", - errorMessage: status.result.errorMessage || "Transaction failed", - }; - default: - return { queueId, status: "Queued" }; - } -} - -// Add a new endpoint to check the status -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const queueId = searchParams.get("queueId"); - - if (!queueId) { - return NextResponse.json({ error: "Missing queueId" }, { status: 400 }); - } - - try { - const result = await pollToMine(queueId); - return NextResponse.json(result); - } catch (error) { - console.error("Error checking transaction status:", error); - return NextResponse.json( - { - status: "error" as TransactionStatus, - error: "Failed to check transaction status", - }, - { status: 500 }, - ); - } -} diff --git a/apps/playground-web/src/app/api/erc20BatchMintTo/route.ts b/apps/playground-web/src/app/api/erc20BatchMintTo/route.ts deleted file mode 100644 index ce001c2cddc..00000000000 --- a/apps/playground-web/src/app/api/erc20BatchMintTo/route.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Engine } from "@thirdweb-dev/engine"; -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import type { Address } from "thirdweb"; - -const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; - -const engine = new Engine({ - url: process.env.ENGINE_URL as string, - accessToken: process.env.ENGINE_ACCESS_TOKEN as string, -}); - -const chain = "84532"; - -type Receiver = { - toAddress: Address; - amount: string; -}; - -type DataEntry = { - toAddress: string; - amount: string; -}; - -export async function POST(req: NextRequest) { - try { - const { data, contractAddress } = await req.json(); - - console.log("Received request with:", { - contractAddress, - dataLength: data.length, - sampleData: data[0], - }); - - const receivers: Receiver[] = data.map((entry: DataEntry) => ({ - toAddress: entry.toAddress as Address, - amount: entry.amount, - })); - - const chunks: Receiver[][] = []; - const chunkSize = 10; - for (let i = 0; i < receivers.length; i += chunkSize) { - chunks.push(receivers.slice(i, i + chunkSize)); - } - - // Process first chunk and return immediately with queued status - const firstChunk = chunks[0]; - const res = await engine.erc20.mintBatchTo( - chain, - contractAddress, - BACKEND_WALLET_ADDRESS, - { - data: firstChunk, - }, - ); - - // Return initial queued status - const initialResult = { - queueId: res.result.queueId, - status: "queued" as const, - addresses: firstChunk.map((r) => r.toAddress), - amounts: firstChunk.map((r) => r.amount), - timestamp: Date.now(), - chainId: Number.parseInt(chain), - network: "Base Sep" as const, - }; - - // Start polling in the background - pollToMine(res.result.queueId).then((pollResult) => { - console.log("Transaction completed:", pollResult); - }); - - return NextResponse.json([initialResult]); - } catch (error: unknown) { - console.error("Detailed error:", error); - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - return NextResponse.json( - { - error: "Transfer failed", - details: errorMessage, - }, - { status: 500 }, - ); - } -} - -async function pollToMine(queueId: string) { - try { - const status = await engine.transaction.status(queueId); - - if (status.result.status === "mined") { - const transactionHash = status.result.transactionHash; - const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`; - return { status: "Mined", queueId, transactionHash, blockExplorerUrl }; - } - - return { - status: - status.result.status.charAt(0).toUpperCase() + - status.result.status.slice(1), - queueId, - }; - } catch (error) { - console.error("Error checking transaction status:", error); - return { - status: "error", - queueId, - errorMessage: "Failed to check transaction status", - }; - } -} diff --git a/apps/playground-web/src/app/api/mintTo/route.ts b/apps/playground-web/src/app/api/mintTo/route.ts deleted file mode 100644 index 4a68ae6338d..00000000000 --- a/apps/playground-web/src/app/api/mintTo/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Engine } from "@thirdweb-dev/engine"; -import { type NextRequest, NextResponse } from "next/server"; - -const CHAIN_ID = "84532"; -const CONTRACT_ADDRESS = "0x8CD193648f5D4E8CD9fD0f8d3865052790A680f6"; -const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; - -const engine = new Engine({ - url: process.env.ENGINE_URL as string, - accessToken: process.env.ENGINE_ACCESS_TOKEN as string, -}); - -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - console.log("Request body:", body); - - const receiver = body.receiver || body.toAddress; - const metadataWithSupply = body.metadataWithSupply; - - if (!receiver || !metadataWithSupply) { - return NextResponse.json( - { error: "Missing receiver or metadataWithSupply" }, - { status: 400 }, - ); - } - - console.log( - `Attempting to mint for receiver: ${receiver}, metadataWithSupply:`, - metadataWithSupply, - ); - console.log("Using CONTRACT_ADDRESS:", CONTRACT_ADDRESS); - - const res = await engine.erc1155.mintTo( - CHAIN_ID, - CONTRACT_ADDRESS, - BACKEND_WALLET_ADDRESS, - { - receiver, - metadataWithSupply, - }, - ); - - // Return immediately with queued status - const initialResult = { - queueId: res.result.queueId, - status: "Queued" as const, - toAddress: receiver, - amount: metadataWithSupply.supply || "1", - timestamp: Date.now(), - chainId: Number.parseInt(CHAIN_ID), - network: "Base Sep" as const, - }; - - // Start polling in the background - pollToMine(res.result.queueId).then((pollResult) => { - // This will be handled by the frontend polling - console.log("Transaction completed:", pollResult); - }); - - return NextResponse.json(initialResult); - } catch (error: unknown) { - console.error("Error minting ERC1155 tokens", error); - if (error instanceof Error) { - return NextResponse.json( - { error: "Error minting ERC1155 tokens", details: error.message }, - { status: 500 }, - ); - } - } -} - -async function pollToMine(queueId: string) { - try { - const status = await engine.transaction.status(queueId); - - if (status.result.status === "mined") { - const transactionHash = status.result.transactionHash; - const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`; - return { status: "Mined", transactionHash, blockExplorerUrl }; - } - - return { status: status.result.status }; - } catch (error) { - console.error("Error checking transaction status:", error); - return { - status: "error", - errorMessage: "Failed to check transaction status", - }; - } -} diff --git a/apps/playground-web/src/app/api/transaction-status/route.ts b/apps/playground-web/src/app/api/transaction-status/route.ts deleted file mode 100644 index b52823f4bf2..00000000000 --- a/apps/playground-web/src/app/api/transaction-status/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Engine } from "@thirdweb-dev/engine"; -import { type NextRequest, NextResponse } from "next/server"; - -const engine = new Engine({ - url: process.env.ENGINE_URL as string, - accessToken: process.env.ACCESS_TOKEN as string, -}); - -export async function GET(req: NextRequest) { - const searchParams = req.nextUrl.searchParams; - const queueId = searchParams.get("queueId"); - - if (!queueId) { - return NextResponse.json({ error: "Missing queueId" }, { status: 400 }); - } - - try { - const status = await engine.transaction.status(queueId); - - if (status.result.status === "mined") { - const transactionHash = status.result.transactionHash; - const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`; - return NextResponse.json({ - status: "Mined", - transactionHash, - blockExplorerUrl, - }); - } - - return NextResponse.json({ - status: status.result.status, - errorMessage: status.result.errorMessage, - }); - } catch (error) { - console.error("Error checking transaction status:", error); - return NextResponse.json( - { error: "Failed to check transaction status" }, - { status: 500 }, - ); - } -} diff --git a/apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx b/apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx index 56d00013f4c..0891c9e962b 100644 --- a/apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx +++ b/apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx @@ -5,7 +5,7 @@ import { ConnectSmartAccountCustomPreview, ConnectSmartAccountPreview, } from "../../../../components/account-abstraction/connect-smart-account"; -import { APIHeader } from "../../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../../components/blocks/APIHeader"; import { CodeExample } from "../../../../components/code/code-example"; export const metadata: Metadata = { @@ -18,24 +18,19 @@ export const metadata: Metadata = { export default function Page() { return ( -
- - Let users connect to their smart accounts with any wallet and - unlock gas sponsorship, batched transactions, session keys and - full wallet programmability. - - } - docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" - heroLink="/account-abstraction.png" - /> - -
- -
-
+ + Let users connect to their smart accounts with any wallet and unlock + gas sponsorship, batched transactions, session keys and full wallet + programmability. + + } + docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" + > + +
); } @@ -43,17 +38,14 @@ export default function Page() { function ConnectSmartAccount() { return ( <> -
-

- Connect smart accounts -

-

- Enable smart accounts on the UI components or build your own UI. -

-
} - code={`// Using UI components + code={`\ import { ConnectButton } from "thirdweb/react"; function App(){ @@ -65,9 +57,15 @@ accountAbstraction={{ chain, sponsorGas: true }} /> };`} lang="tsx" /> + +
} - code={`// Using your own UI + code={`\ import { useConnect } from "thirdweb/react"; import { createWallet } from "thirdweb/wallets"; diff --git a/apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx b/apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx index e558b5de621..97f07cea662 100644 --- a/apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx +++ b/apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx @@ -2,37 +2,31 @@ import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; import type { Metadata } from "next"; import { SponsoredTxZksyncPreview } from "../../../../components/account-abstraction/sponsored-tx-zksync"; -import { APIHeader } from "../../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../../components/blocks/APIHeader"; import { CodeExample } from "../../../../components/code/code-example"; export const metadata: Metadata = { metadataBase, - title: "Sign In, Account Abstraction and SIWE Auth | thirdweb Connect", + title: "Native Account Abstraction", description: - "Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.", + "On zkSync chains, you can take advantage of native account abstraction with no code changes", }; export default function Page() { return ( -
- - Let users connect to their smart accounts with any wallet and - unlock gas sponsorship, batched transactions, session keys and - full wallet programmability. - - } - docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" - heroLink="/account-abstraction.png" - /> - -
- -
-
+ + On zkSync chains, you can take advantage of native account + abstraction with no code changes. + + } + docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" + > + +
); } @@ -40,36 +34,41 @@ export default function Page() { function SponsoredZksyncTx() { return ( <> -
-

- Native Account Abstraction -

-

- On zkSync chains, you can take advantage of native account abstraction - no code changes. The `sponsorGas` option also works out of the box. -

-
} - code={`import { claimTo } from "thirdweb/extensions/erc1155"; - import { TransactionButton } from "thirdweb/react"; + code={`\ +import { claimTo } from "thirdweb/extensions/erc1155"; +import { TransactionButton } from "thirdweb/react"; - function App(){ - return (<> - -{/* since sponsorGas is true, transactions will be sponsored */} - claimTo({ contract, to: "0x123...", tokenId: 0n, quantity: 1n })}>Mint -); -};`} +function App() { + return ( + <> + + {/* since sponsorGas is true, transactions will be sponsored */} + + claimTo({ + contract, + to: "0x123...", + tokenId: 0n, + quantity: 1n, + }) + } + > + Mint + + + ); +}`} lang="tsx" /> diff --git a/apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx b/apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx index 373e2982ffd..3624a6666e2 100644 --- a/apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx +++ b/apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx @@ -2,57 +2,40 @@ import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; import type { Metadata } from "next"; import { SponsoredTxPreview } from "../../../../components/account-abstraction/sponsored-tx"; -import { APIHeader } from "../../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../../components/blocks/APIHeader"; import { CodeExample } from "../../../../components/code/code-example"; export const metadata: Metadata = { metadataBase, - title: "Sign In, Account Abstraction and SIWE Auth | thirdweb Connect", + title: "Sponsored transactions | thirdweb Connect", description: - "Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.", + "Easily enable gas-free transactions for your users, Free on testnets, billed at the end of the month on mainnets.", }; export default function Page() { return ( -
- - Let users connect to their smart accounts with any wallet and - unlock gas sponsorship, batched transactions, session keys and - full wallet programmability. - - } - docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" - heroLink="/account-abstraction.png" - /> - -
- -
-
+ + Easily enable gas-free transactions for your users, Free on + testnets, billed at the end of the month on mainnets. + + } + docsLink="https://portal.thirdweb.com/connect/account-abstraction/overview?utm_source=playground" + > + +
); } function SponsoredTx() { return ( - <> -
-

- Sponsored transactions -

-

- Set `sponsorGas: true` to enable gas-free transactions for your users. -
- Free on testnets, billed at the end of the month on mainnets. -

-
- } - code={`import { claimTo } from "thirdweb/extensions/erc1155"; + } + code={`import { claimTo } from "thirdweb/extensions/erc1155"; import { TransactionButton } from "thirdweb/react"; function App(){ @@ -71,8 +54,7 @@ function SponsoredTx() { claimTo({ contract, to: "0x123...", tokenId: 0n, quantity: 1n })}>Mint ); };`} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/auth/page.tsx b/apps/playground-web/src/app/connect/auth/page.tsx index 65eb7b51689..ed16a899961 100644 --- a/apps/playground-web/src/app/connect/auth/page.tsx +++ b/apps/playground-web/src/app/connect/auth/page.tsx @@ -6,7 +6,7 @@ import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; import type { Metadata } from "next"; import { BasicAuthHookPreview } from "../../../components/auth/basic-auth-hook"; -import { APIHeader } from "../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../components/blocks/APIHeader"; export const metadata: Metadata = { metadataBase, @@ -18,61 +18,37 @@ export const metadata: Metadata = { export default function Page() { return ( -
- - Authenticate users to your backend using only their wallet. This - is a secure and easy way to authenticate users without requiring - them to create an additional account. - - } - docsLink="https://portal.thirdweb.com/typescript/v5/auth?utm_source=playground" - heroLink="/auth.png" - /> - -
+ + Authenticate users to your backend using only their wallet. This is + a secure and easy way to authenticate users without requiring them + to create an additional account. + + } + docsLink="https://portal.thirdweb.com/typescript/v5/auth?utm_source=playground" + > +
-
- -
- -
-
- -
- -
-
- -
- -
-
-
+
+ ); } function BasicAuth() { return ( - <> -
-

- Basic Auth -

-

- Add authentication to your app with a single component. -

-
- - } - code={`"use client"; + } + code={`"use client"; import { generatePayload, @@ -102,28 +78,21 @@ export function AuthButton() { ); } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function BasicAuthHook() { return ( - <> -
-

- Auth with your own UI -

-

- Use the `useConnectModal` hook to add authentication to your app with - your own UI. -

-
- - } - code={`"use client"; + } + code={`"use client"; import { generatePayload, @@ -161,28 +130,21 @@ export function AuthHook() { return ; } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function GatedContent() { return ( - <> -
-

- Gating content with Auth -

-

- Protect your page with thirdweb Auth. Deliver exclusive content to - users who qualify. -

-
- - } - code={`import { THIRDWEB_CLIENT } from "@/lib/client"; + } + code={`import { THIRDWEB_CLIENT } from "@/lib/client"; import { cookies } from "next/headers"; import { getAuthResult } from "@/app/connect/auth/server/actions/auth"; import { hasEnoughBalance } from "..."; @@ -216,27 +178,20 @@ export async function GatedContentPreview() { ); }`} - lang="tsx" - /> - + lang="tsx" + /> ); } function SmartAccountAuth() { return ( - <> -
-

- Smart Account Auth -

-

- Use smart accounts with Sign in with Ethereum (SIWE) -

-
- - } - code={`"use client"; + } + code={`"use client"; import { generatePayload, @@ -271,8 +226,7 @@ export function AuthButton() { ); } `} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/blockchain-api/page.tsx b/apps/playground-web/src/app/connect/blockchain-api/page.tsx index bdec25701c2..b589c52336b 100644 --- a/apps/playground-web/src/app/connect/blockchain-api/page.tsx +++ b/apps/playground-web/src/app/connect/blockchain-api/page.tsx @@ -7,7 +7,7 @@ import { CodeExample } from "@/components/code/code-example"; import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; import type { Metadata } from "next"; -import { APIHeader } from "../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../components/blocks/APIHeader"; export const metadata: Metadata = { metadataBase, @@ -19,67 +19,39 @@ export const metadata: Metadata = { export default function Page() { return ( -
- - Performant, reliable and type safe API to read write to any - contract on any EVM chain through our RPC Edge endpoints. - - } - docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground" - heroLink="/blockchain-api.png" - /> - -
+ + Performant, reliable and type safe API to read write to any contract + on any EVM chain through our RPC Edge endpoints. + + } + docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground" + > +
-
- -
- -
-
- -
- -
-
- -
- -
-
-
- -
-
-
+ +
); } function ReadContractRaw() { return ( - <> -
-

- Query blockchain data -

-

- Read data from any contract or wallet. Type safe functions and hooks - without needing full ABIs. -

-
- - } - code={`import { getContract } from "thirdweb"; + } + code={`import { getContract } from "thirdweb"; import { ethereum } from "thirdweb/chains"; import { MediaRenderer, useReadContract } from "thirdweb/react"; @@ -105,28 +77,21 @@ function App() { ); } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function ReadContractExtension() { return ( - <> -
-

- Prebuilt read extensions -

-

- Extensions let you do more with less code. High level functions with - simple API that do pre and post processing for all common standards. -

-
- - } - code={`import { getContract } from "thirdweb"; + } + code={`import { getContract } from "thirdweb"; import { ethereum } from "thirdweb/chains"; import { MediaRenderer, useReadContract } from "thirdweb/react"; @@ -150,28 +115,21 @@ function App() { ); } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function WriteContractExtension() { return ( - <> -
-

- Prebuilt write extensions -

-

- Extensions let you do more with less code. High level functions with - simple API that do pre and post processing for all common standards. -

-
- - } - code={`import { getContract } from "thirdweb"; + } + code={`import { getContract } from "thirdweb"; import { sepolia } from "thirdweb/chains"; import { claimTo } from "thirdweb/extensions/erc20"; @@ -195,28 +153,21 @@ function App() { } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function WriteContractRaw() { return ( - <> -
-

- Write data to blockchain -

-

- Send transactions with the connected wallet. Type safe functions and - hooks to send contracts call or raw transaction. -

-
- - } - code={`import { getContract, prepareContractCall, toUnits } from "thirdweb"; + } + code={`import { getContract, prepareContractCall, toUnits } from "thirdweb"; import { sepolia } from "thirdweb/chains"; const tw_coin = getContract({ @@ -245,28 +196,21 @@ function App() { } `} - lang="tsx" - /> - + lang="tsx" + /> ); } function WatchEvent() { return ( - <> -
-

- Listen to blockchain events -

-

- Subscribe to any contract event. Auto polling hooks and functions with - type safe event extensions for all common standards. -

-
- - } - code={`import { useContractEvents } from "thirdweb/react"; + } + code={`import { useContractEvents } from "thirdweb/react"; import { getContract } from "thirdweb"; import { base } from "thirdweb/chains"; import { transferEvent } from "thirdweb/extensions/erc20"; @@ -291,8 +235,7 @@ function App() { }); } `} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx b/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx index 6d3cb86bd84..a3d90ae7a3d 100644 --- a/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx +++ b/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx @@ -1,40 +1,51 @@ import { CodeExample } from "@/components/code/code-example"; +import type { Metadata } from "next"; +import { PageLayout } from "../../../../components/blocks/APIHeader"; import { EcosystemConnectEmbed } from "../../../../components/in-app-wallet/ecosystem"; import { Profiles } from "../../../../components/in-app-wallet/profile-sections"; import ThirdwebProvider from "../../../../components/thirdweb-provider"; +import { metadataBase } from "../../../../lib/constants"; + +export const metadata: Metadata = { + metadataBase, + title: "Build your own Ecosystem | thirdweb", + description: + "Build a public or permissioned ecosystem by allowing third party apps and games to connect to the same accounts.", +}; export default function Page() { return ( -
- -
-
+ + Build a public or permissioned ecosystem by allowing third party + apps and games to connect to the same accounts. + + } + docsLink="https://portal.thirdweb.com/connect/wallet/ecosystem/set-up?utm_source=playground" + > + +
-
+
); } -function AnyAuth() { +function ConnectEmbedExample() { return ( - <> -
-

- Build your own Ecosystem -

-

- Build a public or permissioned ecosystem by allowing third party apps - and games to connect to the same accounts. -

-
- - } - code={`import { ecosystemWallet } from "thirdweb/wallets"; + } + code={`import { ecosystemWallet } from "thirdweb/wallets"; import { ConnectEmbed } from "thirdweb/react"; - const wallets = [ // all settings are controlled in your dashboard // including permissions, auth options, etc. @@ -45,8 +56,7 @@ function AnyAuth() { return ( ); };`} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/in-app-wallet/layout.tsx b/apps/playground-web/src/app/connect/in-app-wallet/layout.tsx deleted file mode 100644 index a387f2747db..00000000000 --- a/apps/playground-web/src/app/connect/in-app-wallet/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { metadataBase } from "@/lib/constants"; -import type { Metadata } from "next"; -import { APIHeader } from "../../../components/blocks/APIHeader"; - -export const metadata: Metadata = { - metadataBase, - title: "Sign In, Account Abstraction and SIWE Auth | thirdweb Connect", - description: - "Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.", -}; - -export default function Page(props: { children: React.ReactNode }) { - return ( -
- - Onboard anyone with flexible auth options, secure account recovery, - and smart account integration. - - } - docsLink="https://portal.thirdweb.com/connect/in-app-wallet/overview?utm_source=playground" - heroLink="/in-app-wallet.png" - /> - - {props.children} -
- ); -} diff --git a/apps/playground-web/src/app/connect/in-app-wallet/page.tsx b/apps/playground-web/src/app/connect/in-app-wallet/page.tsx index 855fd308ef5..e08472b56c6 100644 --- a/apps/playground-web/src/app/connect/in-app-wallet/page.tsx +++ b/apps/playground-web/src/app/connect/in-app-wallet/page.tsx @@ -1,44 +1,53 @@ import { CodeExample } from "@/components/code/code-example"; import { CustomLoginForm } from "@/components/in-app-wallet/custom-login-form"; +import type { Metadata } from "next"; +import { PageLayout } from "../../../components/blocks/APIHeader"; import { InAppConnectEmbed } from "../../../components/in-app-wallet/connect-button"; import { Profiles } from "../../../components/in-app-wallet/profile-sections"; import ThirdwebProvider from "../../../components/thirdweb-provider"; +import { metadataBase } from "../../../lib/constants"; + +export const metadata: Metadata = { + metadataBase, + title: "Any Auth | thirdweb in-app wallet", + description: + "Let users sign up with their email, phone number, social media accounts or directly with a wallet", +}; export default function Page() { return ( -
- -
-
+ + Use any of the built-in auth methods or bring your own. +
+ Supports custom auth endpoints to integrate with your existing user + base. + + } + docsLink="https://portal.thirdweb.com/connect/in-app-wallet/overview?utm_source=playground" + > + +
-
+
); } -function AnyAuth() { +function UIIntegration() { return ( - <> -
-

- Any Auth Method -

-

- Use any of the built-in auth methods or bring your own. -
- Supports custom auth endpoints to integrate with your existing user - base. -

-
-
-

Prebuilt UI

-

- Instant out of the box authentication with a prebuilt UI. -

- } - code={`import { inAppWallet } from "thirdweb/wallets"; +
+ } + code={`import { inAppWallet } from "thirdweb/wallets"; import { ConnectEmbed } from "thirdweb/react"; const wallets = [ @@ -70,18 +79,19 @@ function AnyAuth() { return ( ); };`} - lang="tsx" - /> -
-
-

Custom UI

-

- Customize the login UI and integrate with your existing user base. No - limits on customizations and auth methods. -

- } - code={`import { useState } from "react"; + lang="tsx" + /> + +
+ + } + code={`import { useState } from "react"; import { useConnect } from "thirdweb/react"; import { inAppWallet, preAuthenticate } from "thirdweb/wallets/in-app"; @@ -122,11 +132,12 @@ export function CustomLoginUi() { return wallet; }); }; + + return
....
} `} - lang="tsx" - /> -
- + lang="tsx" + /> +
); } diff --git a/apps/playground-web/src/app/connect/in-app-wallet/sponsor/page.tsx b/apps/playground-web/src/app/connect/in-app-wallet/sponsor/page.tsx index a9e9ca602e3..5960673137e 100644 --- a/apps/playground-web/src/app/connect/in-app-wallet/sponsor/page.tsx +++ b/apps/playground-web/src/app/connect/in-app-wallet/sponsor/page.tsx @@ -1,54 +1,80 @@ import { CodeExample } from "@/components/code/code-example"; +import type { Metadata } from "next"; +import { PageLayout } from "../../../../components/blocks/APIHeader"; import { SponsoredInAppTxPreview } from "../../../../components/in-app-wallet/sponsored-tx"; import ThirdwebProvider from "../../../../components/thirdweb-provider"; +import { metadataBase } from "../../../../lib/constants"; + +export const metadata: Metadata = { + metadataBase, + title: "Signless Sponsored Transactions | thirdweb in-app wallet", + description: + "With in-app wallets, users don't need to confirm every transaction. Combine it with smart account flag to cover gas costs for the best UX", +}; export default function Page() { return ( -
+ + With in-app wallets, users {"don't"} need to confirm every + transaction. +
+ Combine it with smart account flag to cover gas costs for the best + UX. + + } + docsLink="https://portal.thirdweb.com/connect/in-app-wallet/overview?utm_source=playground" + > -
+
); } function SponsoredInAppTx() { return ( - <> -
-

- Signless Sponsored Transactions -

-

- With in-app wallets, users don't need to confirm every - transaction. -
- Combine it with smart account flag to cover gas costs for the best UX. -

-
- } - code={`import { inAppWallet } from "thirdweb/wallets"; - import { claimTo } from "thirdweb/extensions/erc1155"; - import { ConnectButton, TransactionButton } from "thirdweb/react"; + } + code={` +import { inAppWallet } from "thirdweb/wallets"; +import { claimTo } from "thirdweb/extensions/erc1155"; +import { + ConnectButton, + TransactionButton, +} from "thirdweb/react"; - const wallets = [ - inAppWallet( - // turn on gas sponsorship for in-app wallets - { smartAccount: { chain, sponsorGas: true }} - ) - ]; +const wallets = [ + inAppWallet( + // turn on gas sponsorship for in-app wallets + { smartAccount: { chain, sponsorGas: true } }, + ), +]; - function App(){ - return (<> - +function App() { + return ( + <> + -{/* signless, sponsored transactions */} - claimTo({ contract, to: "0x123...", tokenId: 0n, quantity: 1n })}>Mint -); -};`} - lang="tsx" - /> + {/* signless, sponsored transactions */} + + claimTo({ + contract, + to: "0x123...", + tokenId: 0n, + quantity: 1n, + }) + } + > + Mint + ); +}`} + lang="tsx" + /> + ); } diff --git a/apps/playground-web/src/app/connect/pay/backend/layout.tsx b/apps/playground-web/src/app/connect/pay/backend/layout.tsx index b3c1e518e8e..7da15deddb0 100644 --- a/apps/playground-web/src/app/connect/pay/backend/layout.tsx +++ b/apps/playground-web/src/app/connect/pay/backend/layout.tsx @@ -1,18 +1,17 @@ import type React from "react"; -import { APIHeader } from "../../../../components/blocks/APIHeader"; +import { PageHeader } from "../../../../components/blocks/APIHeader"; export default function Layout(props: { children: React.ReactNode; }) { return (
- HTTP API to bridge, swap and onramp to and from any currency } docsLink="https://portal.thirdweb.com/connect/pay/overview?utm_source=playground" - heroLink="/ub.png" /> {props.children} diff --git a/apps/playground-web/src/app/connect/pay/backend/page.tsx b/apps/playground-web/src/app/connect/pay/backend/page.tsx index b45e8a57e45..6b78f8b5c39 100644 --- a/apps/playground-web/src/app/connect/pay/backend/page.tsx +++ b/apps/playground-web/src/app/connect/pay/backend/page.tsx @@ -7,10 +7,10 @@ export default async function Page() { try { const paths = await getBridgePaths(); return ( -
-
+
+
-

+

Universal Bridge REST API

@@ -20,7 +20,9 @@ export default async function Page() {

- +
@@ -75,7 +77,7 @@ function BlueprintSection(props: { className="before:absolute before:inset-0" >
-

{item.name}

+

{item.name}

{item.description}

diff --git a/apps/playground-web/src/app/connect/pay/backend/reference/page.tsx b/apps/playground-web/src/app/connect/pay/backend/reference/page.tsx index 1f2fda67f19..68885f61bcc 100644 --- a/apps/playground-web/src/app/connect/pay/backend/reference/page.tsx +++ b/apps/playground-web/src/app/connect/pay/backend/reference/page.tsx @@ -36,11 +36,15 @@ export default async function Page(props: { const title = pathMetadata.summary || ""; return ( -
+
-

- {title} -

+
+ {title && ( +

+ {title} +

+ )} + -
- - Let your users pay for any service with fiat or crypto on any - chain. - - } - docsLink="https://portal.thirdweb.com/connect/pay/get-started?utm_source=playground" - heroLink="/pay.png" - /> - -
- -
-
+ + Let your users pay for any service with fiat or crypto on any chain. + + } + docsLink="https://portal.thirdweb.com/connect/pay/get-started?utm_source=playground" + > + + ); } function BuyMerch() { return ( - <> -
-

- Commerce -

-

- Take payments from Fiat or Crypto directly to your seller wallet. -
- Get notified for every sale through webhooks, which lets you trigger - any action you want like shipping physical goods, activating services - or doing onchain actions. -

-
- - } - code={`import { PayEmbed, getDefaultToken } from "thirdweb/react"; + + Take payments from Fiat or Crypto directly to your seller wallet. +
+ Get notified for every sale through webhooks, which lets you trigger + any action you want like shipping physical goods, activating + services or doing onchain actions. + + ), + }} + preview={} + code={`import { PayEmbed, getDefaultToken } from "thirdweb/react"; import { base } from "thirdweb/chains"; function App() { @@ -78,8 +70,7 @@ function BuyMerch() { /> ); };`} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx b/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx index fdf821e3c0f..144629be160 100644 --- a/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx +++ b/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx @@ -16,9 +16,7 @@ export function CodeGen(props: { code={getCode(props.options)} lang="tsx" loader={} - // Need to add max-h in both places - TODO figure out a better way - className="xl:h-[calc(100vh-100px)]" - scrollableClassName="xl:h-[calc(100vh-100px)]" + className="grow" />
diff --git a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx index 51f0f46cb62..b96239a8364 100644 --- a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx @@ -16,6 +16,7 @@ import type React from "react"; import { useEffect, useState } from "react"; import { defineChain } from "thirdweb/chains"; import { Switch } from "../../../../components/ui/switch"; +import { cn } from "../../../../lib/utils"; import { CollapsibleSection } from "../../sign-in/components/CollapsibleSection"; import { ColorFormGroup } from "../../sign-in/components/ColorFormGroup"; import type { PayEmbedPlaygroundOptions } from "../components/types"; @@ -136,6 +137,7 @@ export function LeftSection(props: { setOptions((v) => ({ @@ -156,6 +158,7 @@ export function LeftSection(props: { id="fund-wallet-chain-id" type="number" placeholder="1 (Ethereum)" + className="bg-card" value={payOptions.buyTokenChain?.id || ""} onChange={(e) => { const chainId = Number.parseInt(e.target.value); @@ -182,28 +185,28 @@ export function LeftSection(props: { setTokenName(e.target.value)} - className={ - needsHighlighting && !tokenName - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && !tokenName && "border-red-500", + )} />
setTokenSymbol(e.target.value)} - className={ - needsHighlighting && !tokenSymbol - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && + !tokenSymbol && + "border-red-500", + )} />
@@ -215,11 +218,12 @@ export function LeftSection(props: { placeholder="0x..." value={tokenAddress} onChange={(e) => setTokenAddress(e.target.value)} - className={ - needsHighlighting && !tokenAddress - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && + !tokenAddress && + "border-red-500", + )} />
@@ -231,6 +235,7 @@ export function LeftSection(props: { placeholder="https://..." value={tokenIcon} onChange={(e) => setTokenIcon(e.target.value)} + className="bg-card" />
@@ -252,7 +257,8 @@ export function LeftSection(props: { setOptions((v) => ({ @@ -272,6 +278,7 @@ export function LeftSection(props: { setOptions((v) => ({ @@ -292,6 +299,7 @@ export function LeftSection(props: { id="direct-payment-chain-id" type="number" placeholder="1 (Ethereum)" + className="bg-card" value={payOptions.buyTokenChain?.id || ""} onChange={(e) => { const chainId = Number.parseInt(e.target.value); @@ -321,11 +329,10 @@ export function LeftSection(props: { placeholder="Name" value={tokenName} onChange={(e) => setTokenName(e.target.value)} - className={ - needsHighlighting && !tokenName - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && !tokenName && "border-red-500", + )} />
@@ -335,11 +342,12 @@ export function LeftSection(props: { placeholder="Symbol" value={tokenSymbol} onChange={(e) => setTokenSymbol(e.target.value)} - className={ - needsHighlighting && !tokenSymbol - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && + !tokenSymbol && + "border-red-500", + )} />
@@ -351,11 +359,12 @@ export function LeftSection(props: { placeholder="0x..." value={tokenAddress} onChange={(e) => setTokenAddress(e.target.value)} - className={ - needsHighlighting && !tokenAddress - ? "border-red-500" - : "" - } + className={cn( + "bg-card", + needsHighlighting && + !tokenAddress && + "border-red-500", + )} />
@@ -367,6 +376,7 @@ export function LeftSection(props: { placeholder="https://..." value={tokenIcon} onChange={(e) => setTokenIcon(e.target.value)} + className="bg-card" />
@@ -414,9 +424,7 @@ export function LeftSection(props: { })) } /> - +
- +
@@ -456,6 +462,7 @@ export function LeftSection(props: { setOptions((v) => ({ @@ -475,6 +482,7 @@ export function LeftSection(props: { setOptions((v) => ({ diff --git a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx index 51932d99b11..18b939a2983 100644 --- a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx @@ -1,6 +1,4 @@ "use client"; - -import { abstractWallet } from "@abstract-foundation/agw-react/thirdweb"; import { usePathname } from "next/navigation"; import { useState } from "react"; import { ZERO_ADDRESS, getContract } from "thirdweb"; @@ -12,7 +10,6 @@ import { lightTheme, useActiveAccount, } from "thirdweb/react"; -import { type WalletId, createWallet } from "thirdweb/wallets"; import { Button } from "../../../../components/ui/button"; import { THIRDWEB_CLIENT } from "../../../../lib/client"; import { cn } from "../../../../lib/utils"; @@ -178,22 +175,6 @@ export function RightSection(props: { ); } -/** - * @internal - */ -export function getWallets(walletIds: WalletId[]) { - const wallets = [ - ...walletIds.map((id) => { - if (id === "xyz.abs") { - return abstractWallet(); - } - return createWallet(id); - }), - ]; - - return wallets; -} - function BackgroundPattern() { const color = "hsl(var(--foreground)/15%)"; return ( @@ -218,7 +199,7 @@ function TabButtons(props: { }) { return (
-
+
{props.tabs.map((tab) => ( - ); - };`} + return ( + + ); +}`} lang="tsx" /> ); } -function Hooks() { +function BuildCustomUISection() { return ( - <> -

- Create custom UI using hooks -

- -

- Full control over your UI using react hooks. -
- Wallet state management is all handled for you. -

+ } + code={`\ +import { useConnect } from "thirdweb/react"; +import { createWallet } from "thirdweb/wallets"; +import { shortenAddress } from "thirdweb/utils"; - } - code={`// Using your own UI - import { useConnect } from "thirdweb/react"; - import { createWallet } from "thirdweb/wallets"; +function App() { + const account = useActiveAccount(); + const wallet = useActiveWallet(); + const { connect, isConnecting, error } = useConnect(); + const { disconnect } = useDisconnect(); - function App(){ - const { connect } = useConnect(); + if (account) { + return ( +
+

Connected: {shortenAddress(account.address)}

+ {wallet && ( + + )} +
+ ); + } - return ( - - ); - };`} - lang="tsx" - /> - + return ( + + ); +}`} + /> ); } diff --git a/apps/playground-web/src/app/connect/social/page.tsx b/apps/playground-web/src/app/connect/social/page.tsx index 2c8f07fcf23..eef99816fcd 100644 --- a/apps/playground-web/src/app/connect/social/page.tsx +++ b/apps/playground-web/src/app/connect/social/page.tsx @@ -3,7 +3,7 @@ import { SocialProfiles } from "@/components/social/social-profiles"; import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; import type { Metadata } from "next"; -import { APIHeader } from "../../../components/blocks/APIHeader"; +import { PageLayout } from "../../../components/blocks/APIHeader"; export const metadata: Metadata = { metadataBase, @@ -15,43 +15,32 @@ export const metadata: Metadata = { export default function Page() { return ( -
- - Gain context about your users and their profiles across other apps - as soon as they sign into your app. - - } - docsLink="https://portal.thirdweb.com/connect?utm_source=playground" // TODO: update this once we have Social API docs - heroLink="/in-app-wallet.png" - /> - -
- -
-
+ + Gain context about your users and their profiles across other apps + as soon as they sign into your app. + + } + docsLink="https://portal.thirdweb.com/connect?utm_source=playground" // TODO: update this once we have Social API docs + > + +
); } function UserProfiles() { return ( - <> -
-

- Social Profiles -

-

- Get users' profiles across apps like ENS, Lens, Farcaster, and - more. -

-
- - } - code={`import { useSocialProfiles, useActiveAccount } from "thirdweb/react"; + } + code={`import { useSocialProfiles, useActiveAccount } from "thirdweb/react"; function App(){ const account = useActiveAccount(); @@ -68,8 +57,7 @@ function UserProfiles() { }
); }; `} - lang="tsx" - /> - + lang="tsx" + /> ); } diff --git a/apps/playground-web/src/app/connect/ui/chain/page.tsx b/apps/playground-web/src/app/connect/ui/chain/page.tsx index 7cf55d15d6e..ec2da96d543 100644 --- a/apps/playground-web/src/app/connect/ui/chain/page.tsx +++ b/apps/playground-web/src/app/connect/ui/chain/page.tsx @@ -1,7 +1,7 @@ -import { APIHeader } from "@/components/blocks/APIHeader"; +import { PageLayout } from "@/components/blocks/APIHeader"; import { - ChainIconBasic, - ChainNameBasic, + ChainIconExample, + ChainNameExample, } from "@/components/headless-ui/chain-examples"; import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; @@ -10,35 +10,23 @@ import type { Metadata } from "next"; export const metadata: Metadata = { metadataBase, title: "Chain Components", - description: - "Enhance your applications with our Chain components, featuring a collection of chain icons, names, and symbols. These customizable components simplify the integration of blockchain information, allowing developers to easily display and manage multiple chains in their user interfaces.", + description: "Headless UI components for rendering chain name and icon", }; export default function Page() { return ( -
- - Enhance your applications with our Chain components, featuring a - collection of chain icons, names, and symbols. These customizable - components simplify the integration of blockchain information, - allowing developers to easily display and manage multiple chains - in their user interfaces. - - } - docsLink="https://portal.thirdweb.com/react/v5/components/onchain#chains?utm_source=playground" - heroLink="/headless-ui-header.png" - /> -
- -
-
- -
-
+ Headless UI components for rendering chain name and icon + } + docsLink="https://portal.thirdweb.com/react/v5/components/onchain#chains?utm_source=playground" + containerClassName="space-y-12" + > + + +
); } diff --git a/apps/playground-web/src/app/connect/ui/nft/page.tsx b/apps/playground-web/src/app/connect/ui/nft/page.tsx index 544dc2c1a01..b93d56923d7 100644 --- a/apps/playground-web/src/app/connect/ui/nft/page.tsx +++ b/apps/playground-web/src/app/connect/ui/nft/page.tsx @@ -1,10 +1,9 @@ -import { APIHeader } from "@/components/blocks/APIHeader"; +import { PageLayout } from "@/components/blocks/APIHeader"; import { - NftCardDemo, + NftCardExample, NftDescriptionBasic, - NftMediaBasic, - NftMediaOverride, - NftNameBasic, + NftMediaExample, + NftNameExample, } from "@/components/headless-ui/nft-examples"; import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; @@ -20,37 +19,20 @@ export const metadata: Metadata = { export default function Page() { return ( -
- - Elevate your NFT applications with our React headless UI - components, engineered for seamless digital asset transactions. - These customizable, zero-styling components simplify NFT - interactions while giving developers complete freedom to craft - their perfect user interface. - - } - docsLink="https://portal.thirdweb.com/react/v5/components/onchain#nfts?utm_source=playground" - heroLink="/headless-ui-header.png" - /> -
- -
-
- -
-
- -
-
+ Headless UI components for rendering NFT Media and metadata + } + docsLink="https://portal.thirdweb.com/react/v5/components/onchain#nfts?utm_source=playground" + > +
+ + -
-
- -
-
+ +
+ ); } diff --git a/apps/playground-web/src/app/connect/ui/page.tsx b/apps/playground-web/src/app/connect/ui/page.tsx index 961d1928617..c973d177198 100644 --- a/apps/playground-web/src/app/connect/ui/page.tsx +++ b/apps/playground-web/src/app/connect/ui/page.tsx @@ -1,16 +1,9 @@ -import { APIHeader } from "@/components/blocks/APIHeader"; +import { PageLayout } from "@/components/blocks/APIHeader"; import { - AccountAddressBasic, - AccountAddressFormat, - AccountAvatarBasic, - AccountBalanceBasic, - AccountBalanceCustomToken, - AccountBalanceFormat, - AccountBalanceUSD, - AccountBlobbieBasic, - AccountNameBasic, - AccountNameCustom, - ConnectDetailsButtonClone, + AccountAvatarExample, + AccountBalanceExample, + AccountBlobbieExample, + AccountNameExample, } from "@/components/headless-ui/account-examples"; import ThirdwebProvider from "@/components/thirdweb-provider"; import { metadataBase } from "@/lib/constants"; @@ -20,61 +13,29 @@ export const metadata: Metadata = { metadataBase, title: "Account Components", description: - "Streamline your Web3 development with our React headless UI components for wallet integration. These unstyled, customizable components handle complex wallet operations while giving you complete control over your dApp's design.", + "Headless components for rendering account information like ENS name, ENS avatar, account balance and more", }; export default function Page() { return ( -
- - Streamline your Web3 development with our React headless UI - components for wallet integration. These unstyled, customizable - components handle complex wallet operations while giving you - complete control over your dApp's design. - - } - docsLink="https://portal.thirdweb.com/react/v5/components/account?utm_source=playground" - heroLink="/headless-ui-header.png" - /> - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
+ + Headless components for rendering account information like ENS name, + ENS avatar, account balance and more + + } + docsLink="https://portal.thirdweb.com/react/v5/components/account?utm_source=playground" + > +
+ + + + +
+
); } diff --git a/apps/playground-web/src/app/connect/ui/token/page.tsx b/apps/playground-web/src/app/connect/ui/token/page.tsx index 4c3dd256b6d..143d02e1f9e 100644 --- a/apps/playground-web/src/app/connect/ui/token/page.tsx +++ b/apps/playground-web/src/app/connect/ui/token/page.tsx @@ -1,8 +1,6 @@ -import { APIHeader } from "@/components/blocks/APIHeader"; +import { PageLayout } from "@/components/blocks/APIHeader"; import { - TokenCard, TokenImageBasic, - TokenImageOverride, TokenNameBasic, TokenSymbolBasic, } from "@/components/headless-ui/token-examples"; @@ -14,44 +12,26 @@ export const metadata: Metadata = { metadataBase, title: "Token Components", description: - "Elevate your ERC20 and native crypto token applications with our React headless UI components, designed for efficient digital currency transactions. These customizable, zero-styling components simplify token interactions, giving developers the flexibility to create their ideal user interface for DeFi platforms, wallets, and other crypto applications.", + "Headless UI components for rendering token image, name, and symbol", }; export default function Page() { return ( -
- - Elevate your ERC20 and native crypto token applications with our - React headless UI components, designed for efficient digital - currency transactions. These customizable, zero-styling components - simplify token interactions, giving developers the flexibility to - create their ideal user interface for DeFi platforms, wallets, and - other crypto applications. - - } - docsLink="https://portal.thirdweb.com/react/v5/components/onchain#tokens?utm_source=playground" - heroLink="/headless-ui-header.png" - /> -
- -
-
- -
-
- -
-
- -
-
- -
-
+ + Headless UI components for rendering token image, name, and symbol + + } + docsLink="https://portal.thirdweb.com/react/v5/components/onchain#tokens?utm_source=playground" + containerClassName="space-y-12" + > + + + +
); } diff --git a/apps/playground-web/src/app/connect/ui/wallet/page.tsx b/apps/playground-web/src/app/connect/ui/wallet/page.tsx index fa8e02765c6..f979dc0a5da 100644 --- a/apps/playground-web/src/app/connect/ui/wallet/page.tsx +++ b/apps/playground-web/src/app/connect/ui/wallet/page.tsx @@ -1,8 +1,7 @@ -import { APIHeader } from "@/components/blocks/APIHeader"; +import { PageLayout } from "@/components/blocks/APIHeader"; import { - WalletIconBasic, - WalletNameBasic, - WalletNameFormat, + WalletIconExample, + WalletNameExample, } from "@/components/headless-ui/wallet-examples"; import ThirdwebProvider from "@/components/thirdweb-provider"; @@ -12,38 +11,23 @@ import type { Metadata } from "next"; export const metadata: Metadata = { metadataBase, title: "Wallet Components", - description: - "Boost your crypto wallet applications with our React headless UI components, optimized for digital asset management. These flexible, unstyled elements simplify cryptocurrency operations while granting developers complete control over the user interface design.", + description: "Headless UI components for rendering wallet name and icon", }; export default function Page() { return ( -
- - Boost your crypto wallet applications with our React headless UI - components, optimized for digital asset management. These - flexible, unstyled elements simplify cryptocurrency operations - while granting developers complete control over the user interface - design. - - } - docsLink="https://portal.thirdweb.com/react/v5/connecting-wallets/ui-components?utm_source=playground" - heroLink="/headless-ui-header.png" - /> -
- -
-
- -
-
- -
-
+ Headless UI components for rendering wallet name and icon + } + docsLink="https://portal.thirdweb.com/react/v5/connecting-wallets/ui-components?utm_source=playground" + containerClassName="space-y-12" + > + + +
); } diff --git a/apps/playground-web/src/app/engine/_hooks/useEngineTxStatus.ts b/apps/playground-web/src/app/engine/_hooks/useEngineTxStatus.ts new file mode 100644 index 00000000000..a33a1ce7509 --- /dev/null +++ b/apps/playground-web/src/app/engine/_hooks/useEngineTxStatus.ts @@ -0,0 +1,56 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { get_engine_tx_status } from "../actions"; +import type { EngineTxStatus } from "../types"; + +export function useEngineTxStatus(queueId: string | undefined) { + return useQuery({ + queryKey: ["engineTxStatus", queueId], + queryFn: async () => { + if (!queueId) throw new Error("No queue ID provided"); + const res = await get_engine_tx_status(queueId); + + const txStatus: EngineTxStatus = { + queueId: queueId, + status: res.result.status, + chainId: res.result.chainId, + transactionHash: res.result.transactionHash, + queuedAt: res.result.queuedAt, + sentAt: res.result.sentAt, + minedAt: res.result.minedAt, + cancelledAt: res.result.cancelledAt, + }; + + return txStatus; + }, + enabled: !!queueId, + refetchInterval(query) { + const status = query.state.data?.status; + const isFinalStatus = + status === "mined" || status === "errored" || status === "cancelled"; + + return isFinalStatus ? false : 2000; + }, + staleTime: 0, + retry: false, + }); +} + +export function useOptimisticallyUpdateEngineTxStatus() { + const queryClient = useQueryClient(); + + return (params: { + chainId: number; + queueId: string; + }) => { + queryClient.setQueryData(["engineTxStatus", params.queueId], { + status: "queued", + chainId: params.chainId.toString(), + queueId: params.queueId, + transactionHash: null, + queuedAt: new Date().toISOString(), + sentAt: null, + minedAt: null, + cancelledAt: null, + } satisfies EngineTxStatus); + }; +} diff --git a/apps/playground-web/src/app/engine/actions.ts b/apps/playground-web/src/app/engine/actions.ts new file mode 100644 index 00000000000..d37b05f825f --- /dev/null +++ b/apps/playground-web/src/app/engine/actions.ts @@ -0,0 +1,86 @@ +"use server"; + +import { Engine } from "@thirdweb-dev/engine"; + +const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string; + +const engine = new Engine({ + url: process.env.ENGINE_URL as string, + accessToken: process.env.ENGINE_ACCESS_TOKEN as string, +}); + +export async function airdrop_tokens_with_engine(params: { + contractAddress: string; + chainId: number; + receivers: { + toAddress: string; + amount: string; + }[]; +}) { + const res = await engine.erc20.mintBatchTo( + params.chainId.toString(), + params.contractAddress, + BACKEND_WALLET_ADDRESS, + { + data: params.receivers, + }, + ); + + return res.result; +} + +export async function get_engine_tx_status(queueId: string) { + const status = await engine.transaction.status(queueId); + return status; +} + +type MintNFTParams = { + contractAddress: string; + chainId: number; + toAddress: string; + metadataWithSupply: { + metadata: { + name: string; + description?: string; + image: string; + }; + supply: string; + }; +}; + +export async function mint_erc1155_nft_with_engine(params: MintNFTParams) { + const res = await engine.erc1155.mintTo( + params.chainId.toString(), + params.contractAddress, + BACKEND_WALLET_ADDRESS, + { + receiver: params.toAddress, + metadataWithSupply: params.metadataWithSupply, + }, + ); + + return res.result; +} + +type ClaimNFTParams = { + contractAddress: string; + chainId: number; + receiverAddress: string; + quantity: number; + tokenId: string; +}; + +export async function claim_erc1155_nft_with_engine(params: ClaimNFTParams) { + const res = await engine.erc1155.claimTo( + params.chainId.toString(), + params.contractAddress, + BACKEND_WALLET_ADDRESS, + { + receiver: params.receiverAddress, + quantity: params.quantity.toString(), + tokenId: params.tokenId, + }, + ); + + return res.result; +} diff --git a/apps/playground-web/src/app/engine/airdrop/_components/airdrop-card.tsx b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-card.tsx new file mode 100644 index 00000000000..1a0abecc68b --- /dev/null +++ b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-card.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { formatDate } from "date-fns"; +import { CheckIcon, ExternalLinkIcon, SendIcon, XIcon } from "lucide-react"; +import { useState } from "react"; +import { Spinner } from "../../../../components/ui/Spinner/Spinner"; +import { cn } from "../../../../lib/utils"; +import type { EngineTxStatus } from "../../types"; +import { airdropExample } from "../constants"; + +export function EngineAirdropCard(props: { + txStatus: EngineTxStatus | undefined; + isAirdropping: boolean; + onStartAirdrop: () => void; +}) { + const [hasSentTxRequest, setHasSentTxRequest] = useState(false); + const getProgressPercentage = () => { + if (!props.txStatus && !props.isAirdropping) { + return 0; + } else if (props.txStatus?.status === "queued") { + return 25; + } else if (props.txStatus?.status === "sent") { + return 75; + } else if (props.txStatus?.status === "mined") { + return 100; + } + }; + + const progressPercentage = getProgressPercentage(); + + const getProgressMessage = () => { + if (!props.txStatus && !props.isAirdropping) { + return "Start the airdrop to distribute tokens to recipients"; + } else if (props.txStatus?.status === "mined") { + return "Airdrop complete! Tokens have been successfully distributed."; + } else { + return "Processing your airdrop. This may take a few moments."; + } + }; + + const airdropState = getAirdropState(props.txStatus, props.isAirdropping); + + const Icon = !airdropState + ? SendIcon + : airdropState?.status === "pending" + ? Spinner + : airdropState?.status === "success" + ? CheckIcon + : XIcon; + + return ( + + +
+ +
+ + + Airdrop + +

{getProgressMessage()}

+
+ +
+ + {airdropState && ( + + )} +
+
+ + {!hasSentTxRequest && ( + + + + )} +
+ ); +} + +type AirdropState = { + title: string; + status: "pending" | "success" | "error"; + timestamp?: { + prefix: string; + value: string; + }; +}; + +function getAirdropState( + txStatus: EngineTxStatus | undefined, + isAirdropping: boolean, +): AirdropState | undefined { + if (!txStatus) { + if (isAirdropping) { + return { + title: "Sending Transaction Request", + status: "pending", + timestamp: { + prefix: "Started at", + value: new Date().toISOString(), + }, + }; + } + return undefined; + } + + if (txStatus.status === "queued") { + return { + title: "Transaction queued", + status: "pending", + timestamp: { + prefix: "Queued at", + value: txStatus.queuedAt as string, + }, + }; + } + + if (txStatus.status === "sent") { + return { + title: "Transaction sent", + status: "pending", + timestamp: { + prefix: "Sent at", + value: txStatus.sentAt as string, + }, + }; + } + + if (txStatus.status === "mined") { + return { + title: "Transaction mined", + status: "success", + timestamp: { + prefix: "Mined at", + value: txStatus.minedAt as string, + }, + }; + } + + if (txStatus.status === "cancelled" || txStatus.status === "errored") { + return { + title: "Transaction failed", + status: "error", + }; + } + + return; +} + +function AirdropStateInfo(props: { + txStatus: EngineTxStatus | undefined; + isAirdropping: boolean; + state: AirdropState; +}) { + const state = props.state; + + return ( +
+
+

{state.title}

+ {state.timestamp && ( +

+ {state.timestamp?.prefix}{" "} + {formatDate( + new Date(state.timestamp.value), + "dd MMM yyyy HH:mm:ss", + )} +

+ )} + + {/* Simplified transaction hash display */} + {props.txStatus?.transactionHash && ( + + )} +
+
+ ); +} diff --git a/apps/playground-web/src/app/engine/airdrop/_components/airdrop-code.tsx b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-code.tsx new file mode 100644 index 00000000000..5419ff200d6 --- /dev/null +++ b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-code.tsx @@ -0,0 +1,82 @@ +import { Code } from "../../../../components/code/code"; +import { airdropExample } from "../constants"; + +export function AirdropCode() { + return ( +
+
+

Code

+

+ Code to implement above shown example +

+
+ +
+

+ Send Airdrop Transaction Request +

+ + +
+ +
+

+ Get Transaction Status +

+

+ Once you send a request to airdrop tokens, you can poll for the status + of the transaction using the following code. +

+
+ +
+ ); +} + +const engineAirdropSendCode = `\ +const chainId = ${airdropExample.chainId}; +const contractAddress = "${airdropExample.contractAddress}"; +const addresses = ${JSON.stringify( + airdropExample.receivers.map((x) => ({ + address: x.toAddress, + quantity: x.amount, + })), + null, + 2, +)}; + +const url = \`\${YOUR_ENGINE_URL}\/contract/\${chainId}/\${contractAddress}\/erc1155\/airdrop\`; + +const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": "Bearer YOUR_SECRET_TOKEN", + "Content-Type": "application/json", + "X-Backend-Wallet-Address": "YOUR_BACKEND_WALLET_ADDRESS", + }, + body: JSON.stringify({ addresses }), +}); + +const data = await response.json(); +const queueId = data.queueId; +`; + +const engineAirdropGetStatus = `\ +function getEngineTxStatus(queueId: string) { + const url = \`\${YOUR_ENGINE_URL}\/transaction/\${queueId}\`; + const response = await fetch(url, { + method: "GET", + headers: { + "Authorization": "Bearer YOUR_SECRET_TOKEN", + }, + }); + + const data = await response.json(); + return data.result; +} + +// you can keep polling for the status until you get a status of either "mined" or "errored" or "cancelled" +const result = await getEngineTxStatus(queueId); + +console.log(result.status); +`; diff --git a/apps/playground-web/src/app/engine/airdrop/_components/airdrop-preview.tsx b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-preview.tsx new file mode 100644 index 00000000000..68cd4e932e3 --- /dev/null +++ b/apps/playground-web/src/app/engine/airdrop/_components/airdrop-preview.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useMutation } from "@tanstack/react-query"; +import { ExternalLinkIcon, EyeIcon, InfoIcon } from "lucide-react"; +import { useState } from "react"; +import { Badge } from "../../../../components/ui/badge"; +import { + useEngineTxStatus, + useOptimisticallyUpdateEngineTxStatus, +} from "../../_hooks/useEngineTxStatus"; +import { airdrop_tokens_with_engine } from "../../actions"; +import { airdropExample } from "../constants"; +import { EngineAirdropCard } from "./airdrop-card"; + +export function EngineAirdropPreview() { + const [queueId, setQueueId] = useState(undefined); + const engineTxStatusQuery = useEngineTxStatus(queueId); + const updateEngineTxStatus = useOptimisticallyUpdateEngineTxStatus(); + const airdropMutation = useMutation({ + mutationFn: async () => { + const response = await airdrop_tokens_with_engine({ + contractAddress: airdropExample.contractAddress, + chainId: airdropExample.chainId, + receivers: airdropExample.receivers, + }); + + return response; + }, + }); + + const handleSubmit = async () => { + const res = await airdropMutation.mutateAsync(); + updateEngineTxStatus({ + chainId: airdropExample.chainId, + queueId: res.queueId, + }); + + setQueueId(res.queueId); + }; + + return ( +
+

Example

+

+ Airdrop tokens to multiple addresses in a single transaction using + thirdweb Engine +

+ +
+
+
+ + +
+
+ +
+ +
+
+
+
+
+ ); +} + +function TabName(props: { + name: string; + icon: React.FC<{ className: string }>; +}) { + return ( +
+ + {props.name} +
+ ); +} + +function AirdropConfig() { + return ( +
+
+

Network

+ + Base Sepolia + +
+ + + +
+

+ Recipients ({airdropExample.receivers.length}) +

+
    + {airdropExample.receivers.map((recipient) => ( +
  • + {recipient.toAddress} +
  • + ))} +
+

+ Each address will receive 1 token +

+
+
+ ); +} diff --git a/apps/playground-web/src/app/engine/airdrop/constants.ts b/apps/playground-web/src/app/engine/airdrop/constants.ts new file mode 100644 index 00000000000..43234d34dbb --- /dev/null +++ b/apps/playground-web/src/app/engine/airdrop/constants.ts @@ -0,0 +1,11 @@ +export const airdropExample = { + contractAddress: "0xcB30dB8FB977e8b27ae34c86aF16C4F5E428c0bE", + chainId: 84532, + chainName: "Base Sepolia", + chainExplorer: "https://base-sepolia.blockscout.com", + receivers: [ + { toAddress: "0x1f91EB653116A43413930c1df0CF5794fCc2D611", amount: "1" }, + { toAddress: "0xA707E9650631800a635c629e9C8E5937b7277a08", amount: "1" }, + { toAddress: "0xF1f466c973C197e5D9318F6241C2da31742d3d03", amount: "1" }, + ], +}; diff --git a/apps/playground-web/src/app/engine/airdrop/page.tsx b/apps/playground-web/src/app/engine/airdrop/page.tsx index 75a93a73eaa..71bd9f371b9 100644 --- a/apps/playground-web/src/app/engine/airdrop/page.tsx +++ b/apps/playground-web/src/app/engine/airdrop/page.tsx @@ -1,46 +1,25 @@ -import { AirdropERC20 } from "@/components/engine/airdrop/airdrop-erc20"; +import { EngineAirdropPreview } from "@/app/engine/airdrop/_components/airdrop-preview"; import ThirdwebProvider from "@/components/thirdweb-provider"; -import { EngineAPIHeader } from "../../../components/blocks/EngineAPIHeader"; -// TODO: Get updated banner image and description. +import { PageLayout } from "../../../components/blocks/APIHeader"; +import { AirdropCode } from "./_components/airdrop-code"; + export default function Page() { return ( -
- - Engine makes it effortless for any developer to airdrop tokens at - scale. You sponsor the gas so your users only need a wallet - address! - - } - deployLink="https://thirdweb.com/team/~/~/engine/create?utm_source=playground" - docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground" - heroLink="/airdrop.avif" - /> - -
- -
-
+ + Engine makes it effortless for any developer to airdrop tokens at + scale. You sponsor the gas so your users only need a wallet address! + + } + docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground" + > + +
+ + ); } - -function InGameCurrency() { - return ( - <> -
-

- Airdrop -

-

- Use Engine to Airdrop in-game currency to a list of players in one - transaction. -

-
- - - ); -} diff --git a/apps/playground-web/src/app/engine/minting/_components/mint-code.tsx b/apps/playground-web/src/app/engine/minting/_components/mint-code.tsx new file mode 100644 index 00000000000..884e3e82805 --- /dev/null +++ b/apps/playground-web/src/app/engine/minting/_components/mint-code.tsx @@ -0,0 +1,83 @@ +import { Code } from "../../../../components/code/code"; +import { mintExample } from "../constants"; + +export function MintCode() { + return ( +
+
+

Code

+

+ Code to implement above shown example +

+
+ +
+

+ Send Transaction Request to Mint NFTs +

+ + +
+ +
+

+ Get Transaction Status +

+

+ Once you send a request to mint NFTs, you can poll for the status of + the transaction using the following code. +

+
+ +
+ ); +} + +const engineMintCode = `\ +const chainId = ${mintExample.chainId}; +const contractAddress = "${mintExample.contractAddress}"; +const url = \`\${YOUR_ENGINE_URL}\/contract/\${chainId}/\${contractAddress}\/erc1155\/mint-to\`; + +const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": "Bearer YOUR_SECRET_TOKEN", + "Content-Type": "application/json", + "X-Backend-Wallet-Address": "YOUR_BACKEND_WALLET_ADDRESS", + }, + body: JSON.stringify({ + receiver: "0x....", + metadataWithSupply: { + metadata: { + name: "...", + description: "...", + image: "...", // ipfs or https link to your asset + }, + supply: "1", + }, + }) +}); + +const data = await response.json(); +console.log(data.queueId); +`; + +const getEngineStatusCode = `\ +function getEngineTxStatus(queueId: string) { + const url = \`\${YOUR_ENGINE_URL}\/transaction/\${queueId}\`; + const response = await fetch(url, { + method: "GET", + headers: { + "Authorization": "Bearer YOUR_SECRET_TOKEN", + }, + }); + + const data = await response.json(); + return data.result; +} + +// you can keep polling for the status until you get a status of either "mined" or "errored" or "cancelled" +const result = await getEngineTxStatus(queueId); + +console.log(result.status); +`; diff --git a/apps/playground-web/src/app/engine/minting/_components/mint-preview.tsx b/apps/playground-web/src/app/engine/minting/_components/mint-preview.tsx new file mode 100644 index 00000000000..efcfaa8cc89 --- /dev/null +++ b/apps/playground-web/src/app/engine/minting/_components/mint-preview.tsx @@ -0,0 +1,386 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { THIRDWEB_CLIENT } from "@/lib/client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CheckIcon, UploadIcon, XIcon } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { upload } from "thirdweb/storage"; +import { isAddress, shortenAddress } from "thirdweb/utils"; +import * as z from "zod"; +import { UploadImage } from "../../../../components/blocks/upload-image"; +import { Spinner } from "../../../../components/ui/Spinner/Spinner"; +import { tryCatch } from "../../../../lib/try-catch"; +import { useEngineTxStatus } from "../../_hooks/useEngineTxStatus"; +import { mint_erc1155_nft_with_engine } from "../../actions"; +import type { EngineTxStatus } from "../../types"; +import { mintExample } from "../constants"; + +const formSchema = z.object({ + name: z.string().min(1, "Required"), + description: z.string().optional(), + walletAddress: z + .string() + .min(1, "Required") + .refine( + (val) => { + // don't directly return `isAddress(val)` to avoid typecasting to `0x{string}` + if (isAddress(val)) { + return true; + } + + return false; + }, + { + message: "Invalid wallet address", + }, + ), + supply: z.coerce.number().int("Must be Integer").min(1, "Required"), + image: z + .instanceof(File, { + message: "Required", + }) + .refine((file) => { + // file must not be larger than 500kb + if (file.size > 500 * 1024) { + return "Image must not be larger than 500kb"; + } + + return true; + }), +}); + +type FormValues = z.infer; + +function TransactionStatusIcon({ + status, + isPending, +}: { status?: string; isPending: boolean }) { + function getIcon() { + if (isPending) { + return ; + } + + switch (status) { + case "mined": + return ; + case "errored": + case "cancelled": + return ; + case "queued": + case "sent": + return ; + default: + return null; + } + } + + const icon = getIcon(); + + if (!icon) { + return null; + } + + return
{icon}
; +} + +function TransactionStatusMessage({ + status, + isPending, +}: { status?: string; isPending: boolean }) { + if (isPending) { + return "Sending Transaction Request"; + } + + switch (status) { + case "queued": + return "Transaction Queued"; + case "mined": + return "Transaction Mined"; + case "sent": + return "Transaction Sent"; + case "errored": + case "cancelled": + return "Transaction Failed"; + default: + return null; + } +} + +function TransactionHashLink({ + hash, + explorer, +}: { hash: string; explorer: string }) { + const shortHash = `${hash.slice(0, 6)}...${hash.slice(-4)}`; + + return ( + + Transaction Hash:{" "} + + {shortHash} + + + ); +} + +function TransactionStatus({ + status, + isPending, + transactionHash, +}: { + status?: string; + isPending: boolean; + transactionHash?: string | null; +}) { + return ( +
+ + +

+ + + + + {transactionHash && ( + + )} +

+
+ ); +} + +export function EngineMintPreview() { + const [queueId, setQueueId] = useState(undefined); + const queryClient = useQueryClient(); + const engineTxStatusQuery = useEngineTxStatus(queueId); + const [hasSentTx, setHasSentTx] = useState(false); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + description: "", + walletAddress: "", + supply: 1, + }, + }); + + const mintMutation = useMutation({ + mutationFn: async (params: { + address: string; + name: string; + description: string; + image: File; + supply: string; + }) => { + const uploadPromise = upload({ + client: THIRDWEB_CLIENT, + files: [params.image], + }); + + const uploadResult = await tryCatch(uploadPromise); + + if (uploadResult.error) { + throw new Error("Failed to upload image"); + } + + return mint_erc1155_nft_with_engine({ + contractAddress: mintExample.contractAddress, + chainId: mintExample.chainId, + toAddress: params.address, + metadataWithSupply: { + metadata: { + name: params.name, + description: params.description, + image: uploadResult.data, + }, + supply: params.supply, + }, + }); + }, + }); + + const onSubmit = async (data: FormValues) => { + setHasSentTx(true); + const result = await mintMutation.mutateAsync({ + address: data.walletAddress, + name: data.name, + description: data.description || "", + image: data.image, + supply: data.supply.toString(), + }); + + // optimistic update + queryClient.setQueryData(["engineTxStatus", result.queueId], { + status: "queued", + chainId: mintExample.chainId.toString(), + queueId: result.queueId, + transactionHash: null, + queuedAt: new Date().toISOString(), + sentAt: null, + minedAt: null, + cancelledAt: null, + } satisfies EngineTxStatus); + + setQueueId(result.queueId); + }; + + return ( +
+

Example

+

+ Mint ERC1155 NFTs in{" "} + + {shortenAddress(mintExample.contractAddress)} + {" "} + contract on {mintExample.chainName} +

+ +
+
+ +
+
+
+ {/* left */} + ( + + Image + + + form.setValue("image", file) + } + className="w-full bg-background lg:w-[300px]" + /> + + + + )} + /> + + {/* right */} +
+ ( + + Name + + + + + + )} + /> + + ( + + Description + +