From c0be05d847c6f7697e675d0f1011e6ef4a8dac14 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Mon, 11 Aug 2025 23:18:12 +1200 Subject: [PATCH 01/12] [Docs] Replace custom API endpoint with OpenAPI component --- .../src/app/ai/chat/EndpointMetadata.tsx | 91 ---- apps/portal/src/app/ai/chat/page.mdx | 22 +- .../page.mdx | 0 apps/portal/src/app/ai/sidebar.tsx | 4 +- apps/portal/src/app/contracts/events/page.mdx | 23 +- apps/portal/src/app/contracts/page.mdx | 84 +--- .../src/app/contracts/transactions/page.mdx | 23 +- apps/portal/src/app/page.tsx | 26 +- .../src/app/transactions/monitor/page.mdx | 12 +- apps/portal/src/app/transactions/page.mdx | 94 +--- .../src/app/transactions/sponsor/page.mdx | 23 +- .../src/app/wallets/custom-auth/page.mdx | 30 +- .../portal/src/app/wallets/get-users/page.mdx | 4 +- apps/portal/src/app/wallets/page.mdx | 54 +-- apps/portal/src/app/wallets/server/page.mdx | 24 +- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 144 +++--- .../APIEndpointMeta/OpenApiEndpoint.tsx | 458 ++++++++++++++++++ .../Document/APIEndpointMeta/index.ts | 6 + .../src/components/Document/Details.tsx | 8 +- apps/portal/src/components/Document/index.ts | 3 +- 20 files changed, 619 insertions(+), 514 deletions(-) delete mode 100644 apps/portal/src/app/ai/chat/EndpointMetadata.tsx rename apps/portal/src/app/ai/chat/{handling-responses => streaming}/page.mdx (100%) create mode 100644 apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx create mode 100644 apps/portal/src/components/Document/APIEndpointMeta/index.ts diff --git a/apps/portal/src/app/ai/chat/EndpointMetadata.tsx b/apps/portal/src/app/ai/chat/EndpointMetadata.tsx deleted file mode 100644 index 8c1c9d0ca17..00000000000 --- a/apps/portal/src/app/ai/chat/EndpointMetadata.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { - type APIParameter, - ApiEndpoint, -} from "@/components/Document/APIEndpointMeta/ApiEndpoint"; -import { secretKeyHeaderParameter } from "../../../components/Document/APIEndpointMeta/common"; - -const contextFilterType = `\ -{ - from: string | null; - chain_ids: string[] | null; - session_id: string | null; -}`; - -const contextParameter: APIParameter = { - description: "Provide additional context information along with the message", - name: "context", - required: false, - type: contextFilterType, - example: { - from: "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", - chain_ids: [8453], - }, -}; - -const response200Example = `\ -{ - "message": "I've prepared a native ETH transfer as requested. Would you like to proceed with executing this transfer?", - "session_id": "123", - "request_id": "456", - "actions": [ - { - "type": "sign_transaction", - "data": { - "chainId": 8453, - "to": "0x1234567890123456789012345678901234567890", - "value": "10000000000000000", - "data": "0x" - }, - "session_id": "123", - "request_id": "456", - "source": "model", - "tool_name": null, - "description": null, - "kwargs": null - } - ] -}`; - -export function EndpointMetadata() { - return ( - - ); -} diff --git a/apps/portal/src/app/ai/chat/page.mdx b/apps/portal/src/app/ai/chat/page.mdx index 97f474c0c81..03191c57b06 100644 --- a/apps/portal/src/app/ai/chat/page.mdx +++ b/apps/portal/src/app/ai/chat/page.mdx @@ -1,23 +1,20 @@ -import { EndpointMetadata } from "./EndpointMetadata"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@doc"; +import { Tabs, TabsContent, TabsList, TabsTrigger, OpenApiEndpoint } from "@doc"; # Chat API -The thirdweb AI chat API is a standard OpenAI-compatible chat completion API that allows you to interact with the thirdweb AI model, specialized for blockchain interactions. - -The thirdweb proprietary model is optimized for blockchain interactions and can: +The thirdweb AI chat API is a standard OpenAI-compatible chat completion API that allows you to interact with the thirdweb AI model, optimized for blockchain interactions. - Query real-time data from the blockchain - Analyze transactions - Fetch token balances, prices and metadata - Prepare any contract call or transaction for signing -- Prepare swaps from any token pair +- Prepare swaps from/to any token pair - Deploy contracts - Generate images -- Search the web for information +- Search the web - And more! -You can use the API with the API directly, or with any OpenAI-compatible client library. +You can use the API with the HTTP API directly, or with any OpenAI-compatible client library. @@ -25,7 +22,7 @@ You can use the API with the API directly, or with any OpenAI-compatible client OpenAI Client - + @@ -50,4 +47,9 @@ chat_completion = client.chat.completions.create( print(chat_completion) ``` - \ No newline at end of file + + +### Going further + +- [Handle streaming responses](/ai/chat/streaming) +- [Full API Reference](https://api.thirdweb-dev.com/reference#tag/ai/post/ai/chat) \ No newline at end of file diff --git a/apps/portal/src/app/ai/chat/handling-responses/page.mdx b/apps/portal/src/app/ai/chat/streaming/page.mdx similarity index 100% rename from apps/portal/src/app/ai/chat/handling-responses/page.mdx rename to apps/portal/src/app/ai/chat/streaming/page.mdx diff --git a/apps/portal/src/app/ai/sidebar.tsx b/apps/portal/src/app/ai/sidebar.tsx index 5ae9dbb8624..bde4af53c03 100644 --- a/apps/portal/src/app/ai/sidebar.tsx +++ b/apps/portal/src/app/ai/sidebar.tsx @@ -13,8 +13,8 @@ export const sidebar: SideBar = { icon: , }, { - name: "Response Handling", - href: "/ai/chat/handling-responses", + name: "Streaming Responses", + href: "/ai/chat/streaming", }, { name: "API Reference", diff --git a/apps/portal/src/app/contracts/events/page.mdx b/apps/portal/src/app/contracts/events/page.mdx index 4afd6688a1b..ca98eaa672e 100644 --- a/apps/portal/src/app/contracts/events/page.mdx +++ b/apps/portal/src/app/contracts/events/page.mdx @@ -3,6 +3,7 @@ import { TabsList, TabsTrigger, TabsContent, + OpenApiEndpoint, } from "@doc"; import { ReactIcon, @@ -31,27 +32,7 @@ Query and listen to contract events for any deployed contract on any EVM chain. - ### Get Contract Events - - You can fetch contract events using the [contract events API](https://api.thirdweb.com/reference#tag/contracts/get/v1/contracts/{address}/events). - - ```http - GET /v1/contracts/{address}/events?chainId=&decode=true - Host: api.thirdweb.com - x-secret-key: - ``` - - Authentication requires either `x-secret-key` (backend) or `x-client-id` (frontend) to be set in the request headers. - - #### Parameters - - - `address` - The contract address - - `chainId` - The chain ID where the contract is deployed - - `decode` - Whether to decode the event data (optional, defaults to false) - - #### Response - - The API returns a list of events that have been emitted by the specified contract, including event details and decoded function calls when `decode=true` is specified. + diff --git a/apps/portal/src/app/contracts/page.mdx b/apps/portal/src/app/contracts/page.mdx index 89e77511c13..096ebaa64d7 100644 --- a/apps/portal/src/app/contracts/page.mdx +++ b/apps/portal/src/app/contracts/page.mdx @@ -10,6 +10,7 @@ import { TabsList, TabsTrigger, TabsContent, + OpenApiEndpoint, } from "@doc"; import { ReactIcon, @@ -67,89 +68,10 @@ Read, write, and deploy smart contracts on any EVM compatible blockchain. - ### Read a Contract - - You can read contract data efficiently using the [contract read API](https://api.thirdweb.com/reference#tag/contracts/post/v1/contracts/read). - - ```http - GET /v1/contracts/read - Host: api.thirdweb.com - Content-Type: application/json - x-client-id: - - { - "chainId": "1" // your chain id - "calls": [{ - "contractAddress": "0x...", - "method": "function allowance(address owner, address spender)", - "params": ["0x...", "0x..."], - }], - } - ``` - - You can batch multiple contract reads in a single request, and the response will be an array of results or errors. - - Authentication requires either `x-secret-key` (backend) or `x-client-id` (frontend) to be set in the request headers. - - ### Write to a Contract - - You can write to a contract using the [contract write API](https://api.thirdweb.com/reference#tag/contracts/post/v1/contracts/write). - - - - Frontend - Backend - - - - On the frontend, use your project client ID and the users's auth token to send a transaction on their behalf. - - ```http - POST /v1/contracts/write - Host: api.thirdweb.com - Content-Type: application/json - x-client-id: - Authorization: Bearer - - { - "from": "0x...", // the user wallet address - "chainId": "1" // the chain id - "calls": [{ - "contractAddress": "0x...", - "method": "function transfer(address to, uint256 amount)", - "params": ["0x...", "1000000000000000000"], - }], - } - ``` - - - - - - On the backend, use your project secret key to send a transaction from any of your server wallets. - - ```http - POST /v1/contracts/write - Host: api.thirdweb.com - Content-Type: application/json - x-secret-key: - - { - "from": "0x...", // your server wallet address - "chainId": "1" // your chain id - "calls": [{ - "contractAddress": "0x...", - "method": "function transfer(address to, uint256 amount)", - "params": ["0x...", "1000000000000000000"], - }], - } - ``` - - - + - You can batch multiple contract writes in a single request, and the transactions will be batched atomically onchain. + diff --git a/apps/portal/src/app/contracts/transactions/page.mdx b/apps/portal/src/app/contracts/transactions/page.mdx index a4a6ac9415a..971e974e92a 100644 --- a/apps/portal/src/app/contracts/transactions/page.mdx +++ b/apps/portal/src/app/contracts/transactions/page.mdx @@ -3,6 +3,7 @@ import { TabsList, TabsTrigger, TabsContent, + OpenApiEndpoint, } from "@doc"; import { EngineIcon, @@ -21,27 +22,7 @@ Query all transactions for any deployed contract on any EVM chain. - ### Get Contract Transactions - - You can fetch all transactions for a contract using the [contract transactions API](https://api.thirdweb.com/reference#tag/contracts/get/v1/contracts/{address}/transactions). - - ```http - GET /v1/contracts/{address}/transactions?chainId=&decode=true - Host: api.thirdweb.com - x-secret-key: - ``` - - Authentication requires either `x-secret-key` (backend) or `x-client-id` (frontend) to be set in the request headers. - - #### Parameters - - - `address` - The contract address - - `chainId` - The chain ID where the contract is deployed - - `decode` - Whether to decode the transaction data (optional, defaults to false) - - #### Response - - The API returns a list of transactions that have interacted with the specified contract, including transaction details and decoded function calls when `decode=true` is specified. + diff --git a/apps/portal/src/app/page.tsx b/apps/portal/src/app/page.tsx index 215b8544222..9b1eb52f4e3 100644 --- a/apps/portal/src/app/page.tsx +++ b/apps/portal/src/app/page.tsx @@ -1,4 +1,10 @@ -import { ArrowUpRightIcon, BotIcon, WebhookIcon, ZapIcon } from "lucide-react"; +import { + ArrowUpRightIcon, + BotIcon, + MessageCircleIcon, + WebhookIcon, + ZapIcon, +} from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { Heading } from "@/components/Document"; @@ -16,7 +22,6 @@ import { BridgeIcon } from "../icons/products/BridgeIcon"; import { ConnectIcon } from "../icons/products/ConnectIcon"; import { EngineIcon } from "../icons/products/EngineIcon"; import { InsightIcon } from "../icons/products/InsightIcon"; -import { PlaygroundIcon } from "../icons/products/PlaygroundIcon"; import DocsHeroDark from "./_images/docs-hero-dark.png"; import DocsHeroLight from "./_images/docs-hero-light.png"; @@ -25,7 +30,7 @@ export default function Page() {
- +
@@ -70,20 +75,19 @@ function Hero() { ); } -function PlaygroundSection() { +function AISection() { return (
- +
- You can track the status of transactions sent via the [transactions API](https://engine.thirdweb.com/reference) using its transaction id. + -```http -GET /v1/transactions/{transactionId} -Host: api.thirdweb.com -Content-Type: application/json -x-secret-key: -``` - -You can also list and search multiple transactions by with the [`/transactions` endpoint](https://api.thirdweb.com/reference#tag/transactions/get/v1/transactions). + diff --git a/apps/portal/src/app/transactions/page.mdx b/apps/portal/src/app/transactions/page.mdx index 76cf0ca03dd..7d90f6135ee 100644 --- a/apps/portal/src/app/transactions/page.mdx +++ b/apps/portal/src/app/transactions/page.mdx @@ -10,6 +10,7 @@ import { TabsList, TabsTrigger, TabsContent, + OpenApiEndpoint, } from "@doc"; import { ReactIcon, @@ -68,96 +69,17 @@ Send, monitor, and manage transactions. Send transactions from user or server wa - ### Send a Transaction - - Send a transaction from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](https://api.thirdweb.com/reference#tag/transactions/post/v1/transactions). - - Transactions can be of type `contract-call`, `encoded` or `native-transfer`, and will be batched atomically onchain. + ### Send raw transactions - - - Frontend - Backend - + Send a raw transaction from a [user wallet](/wallets/users) from the frontend, or [server wallet](/wallets/server) from the backend using the [thirdweb API](https://api.thirdweb.com/reference#tag/transactions/post/v1/transactions). - + - For server wallets, you can execute transactions with just your project secret key. + - For user wallets in React applications that use the SDK, you can obtain the user wallet auth token (JWT) with the [`useAuthToken()`](/references/typescript/v5/useAuthToken) hook. + - For user wallets in TypeScript applications, you can get it by calling `wallet.getAuthToken()` on a connected [`inAppWallet()`](/references/typescript/v5/inAppWallet) or [`ecosystemWallet()`](/references/typescript/v5/ecosystemWallet). - On the frontend, use your project client ID and the users's auth token to send a transaction on their behalf. - - ```http - POST /v1/transactions - Host: api.thirdweb.com - Content-Type: application/json - x-client-id: - # user auth token can be obtained from one of the v1/wallet/user login endpoints - Authorization: Bearer - - { - "from": "0x...", // the user wallet address - "chainId": "1" // the chain id - "transactions": [{ - // contract call - "type": "contract-call", - "contractAddress": "0x...", - "method": "function transfer(address to, uint256 amount)", - "params": ["0x...", "1000000000000000000"], - }, { - // encoded transaction - "type": "encoded", - "to": "0x...", - "data": "0x...", - }, { - // native transfer - "type": "native-transfer", - "to": "0x...", - "value": "1000000000000000000", // in wei - }], - } - ``` - - In React applications that use the SDK, you can obtain the user auth token with the [`useAuthToken()`](/references/typescript/v5/useAuthToken) hook. - - In TypeScript applications, you can get it by calling `wallet.getAuthToken()` on a connected [`inAppWallet()`](/references/typescript/v5/inAppWallet) or [`ecosystemWallet()`](/references/typescript/v5/ecosystemWallet). + - - - - - On the backend, use your project secret key to send a transaction from any of your server wallets. - - ```http - POST /v1/transactions - Host: api.thirdweb.com - Content-Type: application/json - x-secret-key: - - { - "from": "0x...", // the server wallet address - "chainId": "1" // the chain id - "transactions": [{ - // contract call - "type": "contract-call", - "contractAddress": "0x...", - "method": "function transfer(address to, uint256 amount)", - "params": ["0x...", "1000000000000000000"], - }, { - // encoded transaction - "type": "encoded", - "to": "0x...", - "data": "0x...", - }, { - // native transfer - "type": "native-transfer", - "to": "0x...", - "value": "1000000000000000000", // in wei - }], - } - ``` - - - - - + ### Installation diff --git a/apps/portal/src/app/transactions/sponsor/page.mdx b/apps/portal/src/app/transactions/sponsor/page.mdx index fe38f22f178..9a199dac30b 100644 --- a/apps/portal/src/app/transactions/sponsor/page.mdx +++ b/apps/portal/src/app/transactions/sponsor/page.mdx @@ -6,6 +6,7 @@ import { createMetadata, Steps, Step, + OpenApiEndpoint, } from "@doc"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { WalletsSmartIcon } from "@/icons"; @@ -56,22 +57,7 @@ Sponsor gas fees using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), enab EIP-7702 is the default execution mode in the [thirdweb API](https://api.thirdweb.com/reference) for your user and server wallets. -```http -POST /v1/contracts/write -Host: api.thirdweb.com -Content-Type: application/json -x-secret-key: - -{ - "from": "0x...", // your user or server wallet **EOA address** address - "chainId": "1", // your chain id - "calls": [{ - "contractAddress": "0x...", - "method": "function transfer(address to, uint256 amount)", - "params": ["0x...", "1000000000000000000"], - }], -} -``` + @@ -143,11 +129,10 @@ For chains that don't support EIP-7702, you can use EIP-4337 smart contract wall -ERC4337 execution is not supported in the high level thirdweb API, but you can use the lower level [transactions API](https://engine.thirdweb.com/reference#tag/write/post/v1/write/contract) with specific execution options: +ERC4337 execution is not supported in the high level thirdweb API, but you can use the lower level [Engine transactions API](https://engine.thirdweb.com/reference#tag/write/post/v1/write/contract) with specific execution options: ```http -POST /v1/contracts/write -Host: engine.thirdweb.com +POST https://engine.thirdweb.com/v1/write/contract Content-Type: application/json x-secret-key: diff --git a/apps/portal/src/app/wallets/custom-auth/page.mdx b/apps/portal/src/app/wallets/custom-auth/page.mdx index 5332ad44f2f..cdd2bb4edab 100644 --- a/apps/portal/src/app/wallets/custom-auth/page.mdx +++ b/apps/portal/src/app/wallets/custom-auth/page.mdx @@ -5,6 +5,7 @@ import { TabsContent, DocImage, createMetadata, + OpenApiEndpoint, } from "@doc"; import EWCustomAuth from "./images/customauth.png"; import EWCustomAuthdb from "./images/customauthdb.png"; @@ -67,18 +68,7 @@ You will be asked to enter the following values -```http -POST https://api.thirdweb.com/v1/wallets/user/generic-auth -Content-Type: application/json -x-client-id: -x-ecosystem-id: (optional) -x-ecosystem-partner-id: (optional) - -{ - "type": "jwt", - "jwt": "" -} -``` + @@ -198,18 +188,10 @@ Once you've logged in with your own auth, you can pass the user's JWT to the in- -```http -POST https://api.thirdweb.com/v1/wallets/user/generic-auth -Content-Type: application/json -x-client-id: -x-ecosystem-id: (optional) -x-ecosystem-partner-id: (optional) - -{ - "type": "auth-payload", - "payload": "" -} -``` +", +}} /> diff --git a/apps/portal/src/app/wallets/get-users/page.mdx b/apps/portal/src/app/wallets/get-users/page.mdx index 43106125e27..bed7caa5484 100644 --- a/apps/portal/src/app/wallets/get-users/page.mdx +++ b/apps/portal/src/app/wallets/get-users/page.mdx @@ -1,5 +1,5 @@ import { Callout } from "@doc"; -import { createMetadata, ArticleIconCard, Stack } from "@doc"; +import { createMetadata, ArticleIconCard, Stack, OpenApiEndpoint } from "@doc"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { TypeScriptIcon } from "@/icons"; @@ -34,6 +34,8 @@ Host: api.thirdweb.com x-secret-key: ``` + + ### Query Parameters You can then query by different user identifiers: diff --git a/apps/portal/src/app/wallets/page.mdx b/apps/portal/src/app/wallets/page.mdx index 4fa28d6e7d7..4917fd7355f 100644 --- a/apps/portal/src/app/wallets/page.mdx +++ b/apps/portal/src/app/wallets/page.mdx @@ -10,6 +10,8 @@ import { TabsList, TabsTrigger, TabsContent, + OpenApiEndpoint, + Stack, } from "@doc"; import { ReactIcon, @@ -77,56 +79,10 @@ Create user or server wallets, authenticate with your backend, connect to extern You can use the [thirdweb API](https://api.thirdweb.com/reference) to create user wallets. -Authentication requires either `x-secret-key` (backend) or `x-client-id` (frontend) to be set in the request headers. +Once authenticated, the auth endpoints will return the wallet address and a JWT token for usage with the rest of the API. -#### Send a login code to the user - -```http -POST /v1/wallets/user/code -Host: api.thirdweb.com -Content-Type: application/json -x-client-id: - -{ - "type": "email", - "email": "user@example.com" -} -``` - -#### Verify the code and authenticate the user - -```http -POST /v1/wallets/user/code/verify -Host: api.thirdweb.com -Content-Type: application/json -x-client-id: - -{ - "type": "email", - "email": "user@example.com", - "code": "123456", -} -``` - -Once authenticated, the endpoint will return the wallet address and a JWT token for usage with the rest of the API. - - - - - You can create a server wallet with your project secret key and an identifier for the wallet: - - ```http - POST /v1/wallets/server - Host: api.thirdweb.com - Content-Type: application/json - x-secret-key: - - { - "identifier": "treasury-wallet", // your identifier - } - ``` - - This will return the server wallet address to use with the rest of the API. If the wallet already exists, the same wallet will be returned. + + diff --git a/apps/portal/src/app/wallets/server/page.mdx b/apps/portal/src/app/wallets/server/page.mdx index 8d9a3a267d5..523c525c607 100644 --- a/apps/portal/src/app/wallets/server/page.mdx +++ b/apps/portal/src/app/wallets/server/page.mdx @@ -1,4 +1,4 @@ -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Tabs, TabsList, TabsTrigger, TabsContent, OpenApiEndpoint } from "@doc"; import { TypeScriptIcon, EngineIcon } from "@/icons"; import { createMetadata } from "@doc"; @@ -33,27 +33,9 @@ Server wallets are wallets that are managed by your own application, like a trea Once created, you can use your server wallet by passing it as the `from` field of the [thirdweb API](https://api.thirdweb.com/reference#tag/transactions/post/v1/transactions). -### Create a new Server Wallet Programmatically + -```http -POST /v1/wallets/server -Host: api.thirdweb.com -Content-Type: application/json -x-secret-key: - -{ - "identifier": "My Server Wallet" -} -``` - -### List all Server Wallets - -```http -GET /v1/wallets/server -Host: api.thirdweb.com -Content-Type: application/json -x-secret-key: -``` + diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index eff115b8664..8ffc1db3717 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -1,6 +1,8 @@ +import Markdown from "react-markdown"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; +import { Details } from "../Details"; import { Heading } from "../Heading"; import { Paragraph } from "../Paragraph"; import { RequestExample } from "./RequestExample"; @@ -31,7 +33,7 @@ export type APIParameter = type?: string; }; -type ApiEndpointMeta = { +export type ApiEndpointMeta = { title: string; description: React.ReactNode; path: string; @@ -46,7 +48,7 @@ type ApiEndpointMeta = { }; export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { - const { responseExamples } = props.metadata; + const { responseExamples, request } = props.metadata; const requestExamples: Array<{ lang: "javascript" | "bash"; @@ -73,13 +75,11 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
-
-
- - {props.metadata.title} - - {props.metadata.description} -
+
+ + {props.metadata.title} + + {props.metadata.description as string}
@@ -106,6 +106,27 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { method={props.metadata.method} />
+ {/* + TODO: add this back in but as a collapsible section, along with query params +
+ {request.headers.length > 0 && ( + + )} + + {request.pathParameters.length > 0 && ( + + )} + + {request.bodyParameters.length > 0 && ( + + )} +
*/}
@@ -149,61 +170,56 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { ); } -// function ParameterSection(props: { -// title: string; -// parameters: APIParameter[]; -// }) { -// return ( -//
-// -// {props.title} -// -//
-// {props.parameters -// .sort((a, b) => { -// if (a.required === b.required) { -// return 0; -// } -// return a.required ? -1 : 1; -// }) -// .map((param) => ( -// -// ))} -//
-//
-// ); -// } +function ParameterSection(props: { + title: string; + parameters: APIParameter[]; +}) { + return ( +
+
+ {props.title} +
+
+ {props.parameters + .sort((a, b) => { + if (a.required === b.required) { + return 0; + } + return a.required ? -1 : 1; + }) + .map((param) => ( + + ))} +
+
+ ); +} -// function ParameterItem({ param }: { param: APIParameter }) { -// return ( -//
-//
-// {param.description} -// {param.type && ( -//
-//

Type

-// -//
-// )} -//
-//
-// ); -// } +function ParameterItem({ param }: { param: APIParameter }) { + return ( +
+
+ {param.description} + {param.type && ( +
+

Type

+ +
+ )} +
+
+ ); +} function createCurlCommand(params: { metadata: ApiEndpointMeta }) { const url = `${params.metadata.origin}${params.metadata.path}`; @@ -251,6 +267,8 @@ function createFetchCommand(params: { metadata: ApiEndpointMeta }) { for (const param of request.bodyParameters) { if (param.example !== undefined) { bodyObj[param.name] = param.example; + } else { + bodyObj[param.name] = param.type || ""; } } diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx new file mode 100644 index 00000000000..0362221d5db --- /dev/null +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -0,0 +1,458 @@ +import { cache, Suspense } from "react"; +import { + type APIParameter, + ApiEndpoint, + type ApiEndpointMeta, +} from "./ApiEndpoint"; + +// OpenAPI 3.0 types (simplified for our needs) +interface OpenApiSpec { + openapi: string; + info: { + title: string; + version: string; + }; + servers?: Array<{ + url: string; + description?: string; + }>; + paths: { + [path: string]: { + [method: string]: { + summary?: string; + description?: string; + parameters?: Array<{ + name: string; + in: "query" | "header" | "path" | "cookie"; + required?: boolean; + description?: string; + schema?: { + type?: string; + example?: any; + default?: any; + }; + example?: any; + }>; + requestBody?: { + required?: boolean; + content: { + [mediaType: string]: { + schema?: { + type?: string; + properties?: { + [key: string]: { + type?: string; + description?: string; + example?: any; + required?: boolean; + }; + }; + required?: string[]; + example?: any; + examples?: any[]; + }; + }; + }; + }; + responses: { + [statusCode: string]: { + description: string; + content?: { + [mediaType: string]: { + schema?: { + type?: string; + properties?: { + [key: string]: { + type?: string; + description?: string; + example?: any; + required?: boolean; + }; + }; + required?: string[]; + example?: any; + examples?: any[]; + }; + }; + }; + }; + }; + }; + }; + }; +} + +interface OpenApiEndpointProps { + specUrl?: string; + path: string; + method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + requestBodyOverride?: Record; + responseExampleOverride?: Record; +} + +// Cache the OpenAPI spec fetch for 1 hour +const fetchOpenApiSpec = cache(async (url: string): Promise => { + const response = await fetch(url, { + next: { revalidate: 3600 }, // Cache for 1 hour (3600 seconds) + }); + if (!response.ok) { + throw new Error(`Failed to fetch OpenAPI spec: ${response.statusText}`); + } + return response.json(); +}); + +// Recursively generate example values from OpenAPI schema +function generateExampleFromSchema(schema: any, visited = new Set()): any { + // Prevent infinite recursion with circular references + if (visited.has(schema)) { + return {}; + } + + if (!schema || typeof schema !== "object") { + return null; + } + + // If there's an explicit example, use it + if (schema.example !== undefined) { + return schema.example; + } + + // If there are examples array, use the first one + if ( + schema.examples && + Array.isArray(schema.examples) && + schema.examples.length > 0 + ) { + return schema.examples[0]; + } + + // If there's a default value, use it + if (schema.default !== undefined) { + return schema.default; + } + + visited.add(schema); + + try { + switch (schema.type) { + case "string": + if (schema.enum && schema.enum.length > 0) { + return schema.enum[0]; + } + if (schema.format === "date-time") { + return new Date().toISOString(); + } + if (schema.format === "date") { + return new Date().toISOString().split("T")[0]; + } + if (schema.format === "email") { + return "user@example.com"; + } + if (schema.format === "uri" || schema.format === "url") { + return "https://example.com"; + } + return "string"; + + case "number": + case "integer": + if (schema.minimum !== undefined) { + return schema.minimum; + } + if (schema.maximum !== undefined) { + return Math.min(schema.maximum, 100); + } + return schema.type === "integer" ? 0 : 0.0; + + case "boolean": + return true; + + case "array": + if (schema.items) { + const itemExample = generateExampleFromSchema(schema.items, visited); + return [itemExample]; + } + return []; + + case "object": { + const obj: Record = {}; + + if (schema.properties) { + const required = schema.required || []; + + // Always include required properties + for (const requiredProp of required) { + if (schema.properties[requiredProp]) { + obj[requiredProp] = generateExampleFromSchema( + schema.properties[requiredProp], + visited, + ); + } + } + + // Include some optional properties (up to 3 additional ones) + const optionalProps = Object.keys(schema.properties).filter( + (prop) => !required.includes(prop), + ); + const propsToInclude = optionalProps.slice(0, 3); + + for (const prop of propsToInclude) { + obj[prop] = generateExampleFromSchema( + schema.properties[prop], + visited, + ); + } + } + + // If no properties defined but additionalProperties is allowed + if ( + Object.keys(obj).length === 0 && + schema.additionalProperties !== false + ) { + obj.property = "value"; + } + + return obj; + } + + case "null": + return null; + + default: + // Handle allOf, oneOf, anyOf + if (schema.allOf && Array.isArray(schema.allOf)) { + const merged: Record = {}; + for (const subSchema of schema.allOf) { + const subExample = generateExampleFromSchema(subSchema, visited); + if (typeof subExample === "object" && subExample !== null) { + Object.assign(merged, subExample); + } + } + return merged; + } + + if ( + schema.oneOf && + Array.isArray(schema.oneOf) && + schema.oneOf.length > 0 + ) { + return generateExampleFromSchema(schema.oneOf[0], visited); + } + + if ( + schema.anyOf && + Array.isArray(schema.anyOf) && + schema.anyOf.length > 0 + ) { + return generateExampleFromSchema(schema.anyOf[0], visited); + } + + // If we can't determine the type, return a generic object + return schema.properties + ? generateExampleFromSchema( + { type: "object", properties: schema.properties }, + visited, + ) + : "value"; + } + } finally { + visited.delete(schema); + } +} + +function transformOpenApiToApiEndpointMeta( + spec: OpenApiSpec, + path: string, + method: string, + requestBodyOverride?: Record, + responseExampleOverride?: Record, +): ApiEndpointMeta { + const pathItem = spec.paths[path]; + if (!pathItem) { + throw new Error(`Path ${path} not found in OpenAPI spec`); + } + + const operation = pathItem[method.toLowerCase()]; + if (!operation) { + throw new Error(`Method ${method} not found for path ${path}`); + } + + // Get the base URL from servers + const baseUrl = spec.servers?.[0]?.url || ""; + + // Transform parameters + const pathParameters: APIParameter[] = []; + const headers: APIParameter[] = []; + const queryParameters: APIParameter[] = []; + + if (operation.parameters) { + for (const param of operation.parameters) { + const apiParam: APIParameter = { + name: param.name, + description: param.description || "", + type: param.schema?.type, + required: param.required || false, + example: + param.example || param.schema?.example || param.schema?.default, + } as APIParameter; + + switch (param.in) { + case "path": + pathParameters.push(apiParam); + break; + case "header": + headers.push(apiParam); + break; + case "query": + queryParameters.push(apiParam); + break; + } + } + } + + // Transform request body parameters + const bodyParameters: APIParameter[] = []; + + // Use override if provided + if (requestBodyOverride) { + for (const [key, value] of Object.entries(requestBodyOverride)) { + const apiParam: APIParameter = { + name: key, + description: "", + required: false, + example: value, + } as APIParameter; + + bodyParameters.push(apiParam); + } + } else if (operation.requestBody) { + const content = operation.requestBody.content; + const jsonContent = content["application/json"]; + + const example = + jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; + + if (example) { + // If there's a global example but no properties, try to extract from example + if (typeof example === "object" && example !== null) { + for (const [key, value] of Object.entries(example)) { + const apiParam: APIParameter = { + name: key, + description: "", + required: false, + example: value, + } as APIParameter; + + bodyParameters.push(apiParam); + } + } + } else if (jsonContent?.schema?.properties) { + const required = jsonContent.schema.required || []; + + for (const [propName, propSchema] of Object.entries( + jsonContent.schema.properties, + )) { + const prop = propSchema as any; + const apiParam: APIParameter = { + name: propName, + description: prop.description || "", + type: prop.type, + required: required.includes(propName), + example: prop.example, + } as APIParameter; + + bodyParameters.push(apiParam); + } + } + } + + // Transform responses + const responseExamples: Record = {}; + + // Use override if provided, otherwise generate from OpenAPI spec + if (responseExampleOverride) { + Object.assign(responseExamples, responseExampleOverride); + } else { + for (const [statusCode, response] of Object.entries(operation.responses)) { + const content = response.content; + let example = ""; + + if (content) { + const jsonContent = content["application/json"]; + if (jsonContent?.schema) { + // Use the recursive function to generate examples from schema + const generatedExample = generateExampleFromSchema( + jsonContent.schema, + ); + example = JSON.stringify(generatedExample, null, 2); + } else { + example = JSON.stringify({ message: response.description }, null, 2); + } + } else { + example = JSON.stringify({ message: response.description }, null, 2); + } + + responseExamples[statusCode] = example; + } + } + + return { + title: operation.summary || `${method.toUpperCase()} ${path}`, + description: operation.description || operation.summary || "", + path, + origin: baseUrl, + method: method.toUpperCase() as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + request: { + pathParameters, + headers, + bodyParameters, + }, + responseExamples, + }; +} + +function LoadingFallback() { + return ( +
+
+ Loading API documentation... +
+
+ ); +} + +export function OpenApiEndpoint(props: OpenApiEndpointProps) { + return ( + }> + + + ); +} + +async function OpenApiEndpointInner({ + specUrl = "https://api.thirdweb.com/openapi.json", + path, + method = "GET", + requestBodyOverride, + responseExampleOverride, +}: OpenApiEndpointProps) { + try { + const spec = await fetchOpenApiSpec(specUrl); + const metadata = transformOpenApiToApiEndpointMeta( + spec, + path, + method, + requestBodyOverride, + responseExampleOverride, + ); + return ; + } catch (error) { + return ( +
+
+ Failed to load OpenAPI specification:{" "} + {error instanceof Error ? error.message : "Unknown error"} +
+
+ ); + } +} diff --git a/apps/portal/src/components/Document/APIEndpointMeta/index.ts b/apps/portal/src/components/Document/APIEndpointMeta/index.ts new file mode 100644 index 00000000000..25a859a4a04 --- /dev/null +++ b/apps/portal/src/components/Document/APIEndpointMeta/index.ts @@ -0,0 +1,6 @@ +export { + type APIParameter, + ApiEndpoint, + type ApiEndpointMeta, +} from "./ApiEndpoint"; +export { OpenApiEndpoint } from "./OpenApiEndpoint"; diff --git a/apps/portal/src/components/Document/Details.tsx b/apps/portal/src/components/Document/Details.tsx index 6b063115a58..d803e7f8399 100644 --- a/apps/portal/src/components/Document/Details.tsx +++ b/apps/portal/src/components/Document/Details.tsx @@ -22,21 +22,21 @@ export function Details(props: { -

{props.summary} -

+

{props.tags && props.tags.length > 0 && (
{props.tags?.map((flag) => { diff --git a/apps/portal/src/components/Document/index.ts b/apps/portal/src/components/Document/index.ts index fe318889920..a928dee6e36 100644 --- a/apps/portal/src/components/Document/index.ts +++ b/apps/portal/src/components/Document/index.ts @@ -3,6 +3,8 @@ export { Badge } from "../ui/badge"; // export { Table } from "./Table"; export { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +export { ApiEndpoint } from "./APIEndpointMeta/ApiEndpoint"; +export { OpenApiEndpoint } from "./APIEndpointMeta/OpenApiEndpoint"; export { AuthList } from "./AuthList"; export { Breadcrumb } from "./Breadcrumb"; export { Callout } from "./Callout"; @@ -18,7 +20,6 @@ export { GithubButtonLink } from "./GithubButtonLink"; // export { EditPage } from "./EditPage"; export { Grid } from "./Grid"; export { Heading } from "./Heading"; -// export { ApiEndpoint } from "./APIEndpointMeta/ApiEndpoint"; export { InlineCode } from "./InlineCode"; export { InstallTabs } from "./InstallTabs"; export { UnorderedList } from "./List"; From 45f1e8711b5f7a5f67af932ad36299c9e9c2f88c Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Tue, 12 Aug 2025 05:50:56 +0700 Subject: [PATCH 02/12] Revamp wallet and payments API docs and endpoints Updated wallet and payments documentation to reflect new authentication and user management endpoints, including /v1/auth/initiate and /v1/auth/complete. Improved API reference sections, added OpenApiEndpoint usage, clarified pregeneration and user listing flows, and enhanced code samples for Unity and .NET SDKs. Refactored APIEndpoint component to show query parameters and examples, and updated AuthMethodsTabs to use new auth endpoints. --- apps/portal/src/app/contracts/page.mdx | 12 ++ apps/portal/src/app/payments/page.mdx | 25 ++- .../src/app/wallets/custom-auth/page.mdx | 4 +- .../portal/src/app/wallets/get-users/page.mdx | 189 +++++------------- apps/portal/src/app/wallets/page.mdx | 156 +++++---------- .../app/wallets/pregenerate-wallets/page.mdx | 136 ++----------- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 138 +++++++++---- .../APIEndpointMeta/OpenApiEndpoint.tsx | 91 ++++++--- .../components/Document/AuthMethodsTabs.tsx | 8 +- 9 files changed, 316 insertions(+), 443 deletions(-) diff --git a/apps/portal/src/app/contracts/page.mdx b/apps/portal/src/app/contracts/page.mdx index 096ebaa64d7..b5286e7a2a3 100644 --- a/apps/portal/src/app/contracts/page.mdx +++ b/apps/portal/src/app/contracts/page.mdx @@ -69,10 +69,22 @@ Read, write, and deploy smart contracts on any EVM compatible blockchain. + ### Read from Contracts + + ### Write to Contracts + + ### Deploy Contracts + + + + ### List Contracts + + + diff --git a/apps/portal/src/app/payments/page.mdx b/apps/portal/src/app/payments/page.mdx index afc670e9839..8a5c9b0d81f 100644 --- a/apps/portal/src/app/payments/page.mdx +++ b/apps/portal/src/app/payments/page.mdx @@ -10,7 +10,8 @@ import { TabsList, TabsTrigger, TabsContent, - Badge + Badge, + OpenApiEndpoint, } from "@doc"; import { ReactIcon, @@ -181,7 +182,27 @@ const preparedQuote = await Bridge.Buy.prepare({ - The thirdweb Payments REST API is available at `https://payments.thirdweb.com/`. See the full [API Reference](https://payments.thirdweb.com/reference) for details. + Use the thirdweb payments API to create and manage payments programmatically. + + ### Create Payment + + Create a payment for your product or service: + + + + ### Complete Payment + + Complete a payment using the product ID: + + + + ### Swap Tokens + + Swap tokens for optimal payment processing: + + + + For more details, see the full [API Reference](https://api.thirdweb.com/reference). diff --git a/apps/portal/src/app/wallets/custom-auth/page.mdx b/apps/portal/src/app/wallets/custom-auth/page.mdx index cdd2bb4edab..1802c909beb 100644 --- a/apps/portal/src/app/wallets/custom-auth/page.mdx +++ b/apps/portal/src/app/wallets/custom-auth/page.mdx @@ -68,7 +68,7 @@ You will be asked to enter the following values - + @@ -188,7 +188,7 @@ Once you've logged in with your own auth, you can pass the user's JWT to the in- -", }} /> diff --git a/apps/portal/src/app/wallets/get-users/page.mdx b/apps/portal/src/app/wallets/get-users/page.mdx index bed7caa5484..f4aa0180756 100644 --- a/apps/portal/src/app/wallets/get-users/page.mdx +++ b/apps/portal/src/app/wallets/get-users/page.mdx @@ -20,191 +20,104 @@ From your backend, you can list all your users and fetch the details of any user Single User All Users + Create User -### Endpoint +## Get Single User -`GET` request to the following endpoint: - -```http -GET /v1/wallets/user?email=user@example.com -Host: api.thirdweb.com -x-secret-key: -``` - - +Fetch details for a specific user by email, phone, wallet address, or user ID. ### Query Parameters -You can then query by different user identifiers: +You can query by any of these user identifiers: -- `address`: The user's wallet address that thirdweb has generated for them +- `address`: The user's wallet address that thirdweb generated - `email`: The user's email address - `phone`: The user's phone number -- `externalWalletAddress`: The user's wallet address that used to login via SIWE +- `externalWalletAddress`: The user's external wallet address (SIWE login) - `id`: The user's ID (for custom auth) ### Authentication -You need to include the following headers: - +Required headers: - `x-secret-key`: Your secret key for authentication - `x-ecosystem-id` (optional): Your ecosystem ID - `x-ecosystem-partner-id` (optional): Your ecosystem partner ID -### Example Requests - -Here's an example curl command to fetch user details by email: - -```bash +### Example -curl -X GET 'https://api.thirdweb.com/v1/wallets/user?email=user@example.com' \ - -H 'x-secret-key: YOUR_SECRET_KEY' - -``` - -Here's an example curl command to fetch user details by address: + -```bash -curl -X GET 'https://api.thirdweb.com/v1/wallets/user?address=0x123456789abcdef' \ - -H 'x-secret-key: YOUR_SECRET_KEY' -``` +For ecosystem wallets, include the ecosystem headers in your requests. -Here's an example curl command to fetch the user details for an ecosystem owner: + -```bash -curl -X GET 'https://api.thirdweb.com/v1/wallets/user?address=0x123456789abcdef' \ - -H 'x-secret-key: YOUR_SECRET_KEY' \ - -H 'x-ecosystem-id: ecosystem.YOUR_ECOSYSTEM_ID' \ - -H 'x-ecosystem-partner-id: YOUR_PARTNER_ID' -``` + -In both examples, replace `YOUR_SECRET_KEY` with your actual ThirdWeb Client Secret. +## Get All Users -Replace `YOUR_ECOSYSTEM_ID` and `YOUR_PARTNER_ID` with your actual ecosystem ID and partner ID respectively. The partner ID can be one you set up for yourself as the ecosystem owner. +List all users in your in-app or ecosystem wallet with pagination support. -### Response Format +### Query Parameters -The API returns a JSON array with the following structure for each user: +- `page` (optional): Page number for pagination +- `limit` (optional): Maximum users per request (default: 50) -```json -{ - "result": { - "pagination": { - "hasMore": false, - "limit": 50, - "page": 1 - }, - "wallets": [ - { - "address": "0x3aA70e68BBA8BC922a75d07388e368892c15c9Da", - "createdAt": "2025-07-21T22:58:12.834Z", - "profiles": [ - { - "id": "107302390467834615186", - "name": "Richard Hendricks", - "type": "google", - "email": "richard@piedpiper.com", - "picture": "https://lh3.googleusercontent.com/a/ACg8ocKC1D6ezzzaZxxUk4qtK_HCwVwpNamVopazXwklGBwuuHeSf_c=s96-c", - "givenName": "Richard", - "emailVerified": true - } - ] - } - ] - } -} -``` +### Authentication -For more information, [view the full reference](https://api.thirdweb.com/reference#tag/wallets/get/v1/wallets/user) +Required headers: +- `x-secret-key`: Your secret key for authentication +- `x-ecosystem-id` (optional): Your ecosystem ID +- `x-ecosystem-partner-id` (optional): Your ecosystem partner ID -### Convenience Methods + + For ecosystem wallets, the secret key must be from the same account as the ecosystem owner. + -If you are using the thirdweb SDK, you can use the `getUser` method to retrieve user details. +### Example - - - + - + -### Endpoint +## Create User Wallet (Pregeneration) -`GET` request to the following endpoint: +Pregenerate a user wallet before authentication. This creates a wallet in advance that can be claimed later when the user authenticates. -```http -GET /v1/wallets/users -Host: api.thirdweb.com -x-secret-key: -``` +### Use Cases -### Query Parameters - -- `page` (optional): The page number to fetch (for pagination) -- `limit` (optional): Maximum number of users to return per request (defaults to 50) +- Create wallets based on known email addresses or user IDs +- Pre-fund wallets with tokens or NFTs before users claim them +- Enable smoother onboarding experiences ### Authentication -You need to include the following headers: - +Required headers: - `x-secret-key`: Your secret key for authentication - `x-ecosystem-id` (optional): Your ecosystem ID - `x-ecosystem-partner-id` (optional): Your ecosystem partner ID - - For ecosystem wallets, the secret key have to be from the same account as the - ecosystem owner. - +### Example -### Example Requests - -Here's an example curl command to fetch all users: - -```bash -curl -X GET 'https://api.thirdweb.com/v1/wallets/users?page=2&limit=100' \ - -H 'x-secret-key: YOUR_SECRET_KEY' \ - -H 'Content-Type: application/json' -``` - -### Response Format - -A successful API call returns an array of user objects in the following format: - -```json -{ - "result": { - "pagination": { - "hasMore": false, - "limit": 50, - "page": 1 - }, - "wallets": [ - { - "address": "0x3aA70e68BBA8BC922a75d07388e368892c15c9Da", - "createdAt": "2025-07-21T22:58:12.834Z", - "profiles": [ - { - "id": "107302390467834615186", - "name": "Richard Hendricks", - "type": "google", - "email": "richard@piedpiper.com", - "picture": "https://lh3.googleusercontent.com/a/ACg8ocKC1D6ezzzaZxxUk4qtK_HCwVwpNamVopazXwklGBwuuHeSf_c=s96-c", - "givenName": "Richard", - "emailVerified": true - }] - }, - ] -} -``` + + + +## SDK Integration + +If you're using the thirdweb SDK, you can use the `getUser` method for easier integration: + + + + diff --git a/apps/portal/src/app/wallets/page.mdx b/apps/portal/src/app/wallets/page.mdx index 4917fd7355f..907e7a7d04c 100644 --- a/apps/portal/src/app/wallets/page.mdx +++ b/apps/portal/src/app/wallets/page.mdx @@ -34,12 +34,7 @@ export const metadata = createMetadata({ # Get Started -Create user or server wallets, authenticate with your backend, connect to external wallets, and more. User wallets can be created using different authentication methods: - -- verification code (email, phone, etc.) -- oauth (google, apple, etc.) -- passkey -- sign in with ethereum +Create wallets for your users with flexible authentication options. Choose from email/phone verification, social OAuth, passkeys, or external wallet connections. @@ -75,14 +70,24 @@ Create user or server wallets, authenticate with your backend, connect to extern -### API Usage +## API Authentication + +Create and authenticate user wallets using different methods: + +### Initiate Authentication +Start authentication with email, phone, passkey, or social providers: + + + +### Complete Authentication +Verify and complete the authentication process: -You can use the [thirdweb API](https://api.thirdweb.com/reference) to create user wallets. + -Once authenticated, the auth endpoints will return the wallet address and a JWT token for usage with the rest of the API. +### Get Wallet Information +Retrieve authenticated user's wallet details: - - + @@ -351,66 +356,17 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT ### Installation - 1. Download the latest [thirdweb Unity SDK](https://github.com/thirdweb-dev/unity-sdk/releases) (.unitypackage file) + 1. Download the latest [thirdweb Unity SDK](https://github.com/thirdweb-dev/unity/releases) (.unitypackage file) 2. Import the package into your Unity project via Assets > Import Package > Custom Package ### Configure Client ID After importing the SDK: - 1. Go to Project Settings > Thirdweb - 2. Enter your Client ID from the thirdweb dashboard + 1. Drag the ThirdwebManager prefab into your scene. + 2. Enter your Client ID and Bundle ID from the thirdweb dashboard 3. Allowlist your game's Bundle ID on the thirdweb dashboard for security - ### Initialize the SDK - - Create a new script to manage wallet connections: - - ```csharp - using Thirdweb; - using UnityEngine; - using UnityEngine.UI; - - public class WalletManager : MonoBehaviour - { - private ThirdwebSDK sdk; - public Text walletAddressText; - public Button connectButton; - - void Start() - { - // Client ID is set in Project Settings > Thirdweb - sdk = new ThirdwebSDK("ethereum"); // Or any supported chain - connectButton.onClick.AddListener(ConnectWallet); - } - - public async void ConnectWallet() - { - try { - // Connect with an external wallet like Coinbase Wallet - string address = await sdk.wallet.Connect(new WalletConnection() { - provider = WalletProvider.CoinbaseWallet, - chainId = 1 // Ethereum Mainnet - }); - - walletAddressText.text = "Connected: " + address; - } - catch (System.Exception e) { - Debug.LogError("Error connecting wallet: " + e.Message); - } - } - } - ``` - - ### Using the Connect Wallet Prefab - - For a quicker implementation, use the provided prefab: - - 1. Add the `ThirdwebManager` prefab to your scene - 2. Configure your Client ID in the inspector - 3. Add the `ConnectWallet` prefab to your UI Canvas - 4. Connect the prefab to your ThirdwebManager - ### Implementing In-App Wallets Enable email login in Unity: @@ -419,17 +375,13 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT public async void ConnectWithEmail(string email) { try { - string address = await sdk.wallet.Connect(new WalletConnection() { - provider = WalletProvider.EmbeddedWallet, - email = email, - chainId = 1 // Ethereum Mainnet - }); - - walletAddressText.text = "Connected: " + address; - - // Read wallet balance - var balance = await sdk.wallet.GetBalance(); - Debug.Log($"Balance: {balance.DisplayValue} {balance.Symbol}"); + var inAppWalletOptions = new InAppWalletOptions(email: "myepicemail@domain.id"); + var options = new WalletOptions( + provider: WalletProvider.InAppWallet, + chainId: 1, + inAppWalletOptions: inAppWalletOptions + ); + var wallet = await ThirdwebManager.Instance.ConnectWallet(options); } catch (System.Exception e) { Debug.LogError("Error connecting wallet: " + e.Message); @@ -454,18 +406,15 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT ```csharp using Thirdweb; - - // For client-side applications: - var sdk = new ThirdwebSDK("ethereum", new ThirdwebSDK.Options - { - ClientId = "YOUR_CLIENT_ID" // From thirdweb dashboard - }); - - // For server-side applications: - // var sdk = new ThirdwebSDK("ethereum", new ThirdwebSDK.Options - // { - // SecretKey = Environment.GetEnvironmentVariable("THIRDWEB_SECRET_KEY") - // }); + + // For native applications + var client = ThirdwebClient.Create(clientId: "yourClientId", bundleId: "yourBundleId"); + + // For frontend applications + var client = ThirdwebClient.Create(clientId: "yourClientId"); + + // For backend applications + var client = ThirdwebClient.Create(secretKey: "yourSecretKey"); ``` ### Connect External Wallets @@ -473,17 +422,15 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT For .NET applications that need to interact with external wallets: ```csharp - // For server-side applications or wallet management - var privateKey = Environment.GetEnvironmentVariable("WALLET_PRIVATE_KEY"); // Never hardcode - var wallet = new PrivateKeyWallet(privateKey); + var privateKeyHex = "yourPrivateKeyHex"; // Should be securely stored and accessed + var wallet = await PrivateKeyWallet.Create(client, privateKeyHex); - await sdk.SetWallet(wallet); - var address = await sdk.Wallet.GetAddress(); + var address = await wallet.GetAddress(); Console.WriteLine($"Connected wallet address: {address}"); // Read wallet balance - var balance = await sdk.Wallet.GetBalance(); - Console.WriteLine($"Balance: {balance.DisplayValue} {balance.Symbol}"); + var balance = await wallet.GetBalance(chainId); + Console.WriteLine($"Balance: {balance.ToString().ToEth()}"); ``` ### Using In-App Wallets @@ -491,20 +438,9 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT Create wallets with email authentication: ```csharp - // Create an embedded wallet with email - var walletOptions = new EmbeddedWalletOptions - { - Email = "user@example.com", - AuthProvider = AuthProvider.Email - }; - - var wallet = new EmbeddedWallet(walletOptions); - await sdk.SetWallet(wallet); - - // Authenticate and get the wallet address - await wallet.Authenticate(); - var address = await sdk.Wallet.GetAddress(); - Console.WriteLine($"Connected with embedded wallet: {address}"); + var wallet = await InAppWallet.Create(client: client, email: "userEmail"); + await wallet.SendOTP(); // and fetch the otp + var address = await wallet.LoginWithOtp("userEnteredOTP"); // try catch and retry if needed ``` ### Reading Contract Data @@ -514,9 +450,9 @@ Once authenticated, the auth endpoints will return the wallet address and a JWT ```csharp // Get a contract instance var contract = await ThirdwebContract.Create( - client: sdk.Client, - address: "0x...", - chain: Chain.Ethereum + client, + "contractAddress", + chainId, ); // Read a value from the contract diff --git a/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx b/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx index a9d5d30ed99..8698e3be511 100644 --- a/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx +++ b/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx @@ -1,4 +1,4 @@ -import { Callout } from "@doc"; +import { Callout, OpenApiEndpoint } from "@doc"; import { createMetadata, ArticleIconCard } from "@doc"; export const metadata = createMetadata({ @@ -12,132 +12,30 @@ export const metadata = createMetadata({ # Pregenerate Wallets -### What is Wallet Pregeneration? +Create wallets for users before they authenticate, enabling smoother onboarding and pre-funded asset distribution. -Wallet pregeneration is a technique where you create wallets for users before they actually sign up or authenticate. This allows you to: +## Use Cases -1. Pre assign wallets to users before they sign up -2. Reduce the time users spend waiting during the onboarding process -3. Pre-fund wallets with tokens or NFTs before users claim them -4. Create smoother user experiences for gaming and other applications +- **Pre-assign wallets** to users before signup +- **Reduce onboarding time** by eliminating wallet creation delays +- **Pre-fund wallets** with tokens or NFTs before users claim them +- **Create smooth experiences** for gaming and app onboarding +- **Distribute assets** like game items, welcome bonuses, or promotional NFTs -### Why Use Wallet Pregeneration? +## API Reference -You can distribute assets to wallets before users claim them, enabling: -- Pre-loaded game assets -- Welcome bonus tokens -- Promotional NFTs -- Airdrops + ---- - -## Endpoint - -To pregenerate an in-app or ecosystem wallet wallet, you can make a `POST` request to the following endpoint: - -```http -POST https://api.thirdweb.com/v1/wallets/user/pregenerate -Content-Type: application/json -x-secret-key: -x-ecosystem-id: (optional) -x-ecosystem-partner-id: (optional) - -{ - "type": "email", - "email": "user@example.com" -} -``` - -## Request Body - -The request body should be a JSON object with the following parameters: - -- `type`: The type of wallet identifier to pregenerate a wallet for: - - Email based: `email`, `google`, `facebook`, `discord` - - Phone based: `phone` - - Wallet based: `signer` - - User ID based: `custom_jwt` or `custom_auth_endpoint` -- `email` or `phone` or `userId` or `walletAddress`: The user identifier associated with the wallet to be generated - -### Email based wallets - -``` -{ type: "email", email: "user@example.com" } -``` - -When the user logs in with any method associated with that email (including google, facebook, discord auth), they will get access to the same pregenerated wallet. - -### Phone based wallets - -``` -{ type: "phone", phone: "+1321123321" } -``` - -### Custom user id based wallets - -``` -{ type: "custom_auth_endpoint", userId: "some_user_id" } -``` - -{/* TODO: update link when custom auth documentation has been updated */} -Use this when [bringing your own authentication method](/wallets/custom-auth). When the user logs in, if the user ids you provide from the auth endpoint match, they will get access to the same pregenerated wallet. - -## Headers - -You need to include the following headers: - -- `Content-Type`: Must be set to `application/json` -- `x-secret-key`: Your secret key for authentication -- `x-ecosystem-id` (optional): Your ecosystem ID -- `x-ecosystem-partner-id` (optional): Your ecosystem partner ID - -## Example Request - -Here's an example curl command to pregenerate a thirdweb wallet for the user `user@example.com`: - -```bash -curl -X POST 'https://api.thirdweb.com/v1/wallets/user/pregenerate' \ - -H 'x-ecosystem-id: ecosystem.example-eco-123' \ - -H 'x-ecosystem-partner-id: 1415d24e-c7b0-4fce-846e-740841ef2c32' \ - -H 'x-secret-key: ' \ - -H 'Content-Type: application/json' \ - -d '{ - "type": "email", - "email": "user@example.com" - }' -``` - -Replace the header values with your actual client ID, ecosystem ID, and secret key. - - -For ecosystem wallets, the secret key have to be from the same account as the ecosystem owner. If this is an issue, please reach out to us. + +For ecosystem wallets, the secret key must be from the same account as the ecosystem owner. - -## Response Format - -A successful API call returns a JSON object in the following format: - -```json -{ - "result": { - "address": "string", - "createdAt": "string", - "profile": "object[]" - } -} -``` - - -Use [`predictSmartAccountAddress`](/references/typescript/v5/predictSmartAccountAddress) to generate the corresponding smart contract wallet address. -The `address` field always corresponds to the EOA address (admin wallet) that has been pregenerated. + +The API returns the EOA (admin wallet) address. Use [`predictSmartAccountAddress`](/references/typescript/v5/predictSmartAccountAddress) to get the corresponding smart contract wallet address. +## Next Steps -## What's Next - -Pre-generating is independent and doesn't change the user's experience. - -Your users can continue to login as per usual. When they do, they will be assigned the pregenerated wallet. +Pregeneration doesn't change the user experience. Users can login normally and will automatically receive their pregenerated wallet. -For more information on signing in, see the [User Wallets](/wallets/users) section. +For more information, see the [User Wallets](/wallets/users) documentation. diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index 8ffc1db3717..c14ed6709f1 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -1,4 +1,3 @@ -import Markdown from "react-markdown"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; @@ -42,6 +41,7 @@ export type ApiEndpointMeta = { request: { pathParameters: APIParameter[]; headers: APIParameter[]; + queryParameters: APIParameter[]; bodyParameters: APIParameter[]; }; responseExamples: Record; @@ -75,13 +75,6 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
-
- - {props.metadata.title} - - {props.metadata.description as string} -
-
Request @@ -106,27 +99,33 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { method={props.metadata.method} />
- {/* - TODO: add this back in but as a collapsible section, along with query params +
- {request.headers.length > 0 && ( - - )} - - {request.pathParameters.length > 0 && ( - - )} - - {request.bodyParameters.length > 0 && ( - - )} -
*/} + {request.headers.length > 0 && ( + + )} + + {request.pathParameters.length > 0 && ( + + )} + + {request.queryParameters.length > 0 && ( + + )} + + {request.bodyParameters.length > 0 && ( + + )} +
@@ -200,20 +199,55 @@ function ParameterItem({ param }: { param: APIParameter }) {
+ {param.name} + {param.type && ( + + {param.type} + + )} +
+ } tags={param.required ? ["Required"] : []} > -
- {param.description} +
+ {param.description && ( +
+
Description
+ {param.description} +
+ )} + {param.type && ( -
-

Type

- +
+
Type
+
+ +
+
+ )} + + {param.example !== undefined && ( +
+
Example
+
+ +
)}
@@ -222,9 +256,19 @@ function ParameterItem({ param }: { param: APIParameter }) { } function createCurlCommand(params: { metadata: ApiEndpointMeta }) { - const url = `${params.metadata.origin}${params.metadata.path}`; + let url = `${params.metadata.origin}${params.metadata.path}`; const bodyObj: Record = {}; + // Add query parameters to URL + const queryParams = params.metadata.request.queryParameters + .filter((q) => q.example !== undefined) + .map((q) => `${q.name}=${encodeURIComponent(String(q.example))}`) + .join("&"); + + if (queryParams) { + url += `?${queryParams}`; + } + const headers = params.metadata.request.headers .filter((h) => h.example !== undefined) .map((h) => { @@ -256,7 +300,17 @@ function createFetchCommand(params: { metadata: ApiEndpointMeta }) { const headersObj: Record = {}; const bodyObj: Record = {}; const { request } = params.metadata; - const url = `${params.metadata.origin}${params.metadata.path}`; + let url = `${params.metadata.origin}${params.metadata.path}`; + + // Add query parameters to URL + const queryParams = request.queryParameters + .filter((q) => q.example !== undefined) + .map((q) => `${q.name}=${encodeURIComponent(String(q.example))}`) + .join("&"); + + if (queryParams) { + url += `?${queryParams}`; + } for (const param of request.headers) { if (param.example !== undefined) { @@ -281,7 +335,7 @@ function createFetchCommand(params: { metadata: ApiEndpointMeta }) { } if (Object.keys(bodyObj).length > 0) { - fetchOptions.body = bodyObj; + fetchOptions.body = JSON.stringify(bodyObj); } return `fetch('${url}', ${JSON.stringify(fetchOptions, null, 2)})`; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 0362221d5db..672f66e30c6 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -86,7 +86,7 @@ interface OpenApiEndpointProps { specUrl?: string; path: string; method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - requestBodyOverride?: Record; + requestBodyOverride?: Record; responseExampleOverride?: Record; } @@ -101,7 +101,27 @@ const fetchOpenApiSpec = cache(async (url: string): Promise => { return response.json(); }); +// Helper function to get example values based on type +function getExampleForType(type?: string): unknown { + switch (type) { + case "string": + return "string"; + case "number": + case "integer": + return 0; + case "boolean": + return true; + case "array": + return []; + case "object": + return {}; + default: + return undefined; + } +} + // Recursively generate example values from OpenAPI schema +// eslint-disable-next-line @typescript-eslint/no-explicit-any function generateExampleFromSchema(schema: any, visited = new Set()): any { // Prevent infinite recursion with circular references if (visited.has(schema)) { @@ -174,6 +194,7 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { return []; case "object": { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const obj: Record = {}; if (schema.properties) { @@ -220,6 +241,7 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { default: // Handle allOf, oneOf, anyOf if (schema.allOf && Array.isArray(schema.allOf)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const merged: Record = {}; for (const subSchema of schema.allOf) { const subExample = generateExampleFromSchema(subSchema, visited); @@ -263,7 +285,7 @@ function transformOpenApiToApiEndpointMeta( spec: OpenApiSpec, path: string, method: string, - requestBodyOverride?: Record, + requestBodyOverride?: Record, responseExampleOverride?: Record, ): ApiEndpointMeta { const pathItem = spec.paths[path]; @@ -289,10 +311,13 @@ function transformOpenApiToApiEndpointMeta( const apiParam: APIParameter = { name: param.name, description: param.description || "", - type: param.schema?.type, + type: param.schema?.type || "string", required: param.required || false, example: - param.example || param.schema?.example || param.schema?.default, + param.example || + param.schema?.example || + param.schema?.default || + getExampleForType(param.schema?.type), } as APIParameter; switch (param.in) { @@ -328,40 +353,53 @@ function transformOpenApiToApiEndpointMeta( const content = operation.requestBody.content; const jsonContent = content["application/json"]; - const example = - jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; - - if (example) { - // If there's a global example but no properties, try to extract from example - if (typeof example === "object" && example !== null) { - for (const [key, value] of Object.entries(example)) { - const apiParam: APIParameter = { - name: key, - description: "", - required: false, - example: value, - } as APIParameter; - - bodyParameters.push(apiParam); - } - } - } else if (jsonContent?.schema?.properties) { + if (jsonContent?.schema?.properties) { const required = jsonContent.schema.required || []; for (const [propName, propSchema] of Object.entries( jsonContent.schema.properties, )) { - const prop = propSchema as any; + const prop = propSchema as { + type?: string; + description?: string; + example?: string | number | boolean | object; + format?: string; + enum?: (string | number | boolean)[]; + }; + + // Generate example if not provided + let example = prop.example; + if (example === undefined) { + example = generateExampleFromSchema(prop); + } + const apiParam: APIParameter = { name: propName, description: prop.description || "", - type: prop.type, + type: prop.type || "string", required: required.includes(propName), - example: prop.example, + example: example, } as APIParameter; bodyParameters.push(apiParam); } + } else { + // Fallback: try to generate from the schema example + const example = + jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; + + if (example && typeof example === "object" && example !== null) { + for (const [key, value] of Object.entries(example)) { + const apiParam: APIParameter = { + name: key, + description: "", + required: false, + example: value, + } as APIParameter; + + bodyParameters.push(apiParam); + } + } } } @@ -397,13 +435,14 @@ function transformOpenApiToApiEndpointMeta( return { title: operation.summary || `${method.toUpperCase()} ${path}`, - description: operation.description || operation.summary || "", + description: "", // Always empty since we don't want to show titles/descriptions path, origin: baseUrl, method: method.toUpperCase() as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", request: { pathParameters, headers, + queryParameters, bodyParameters, }, responseExamples, diff --git a/apps/portal/src/components/Document/AuthMethodsTabs.tsx b/apps/portal/src/components/Document/AuthMethodsTabs.tsx index dc21f7b1179..c0fd3fec1ab 100644 --- a/apps/portal/src/components/Document/AuthMethodsTabs.tsx +++ b/apps/portal/src/components/Document/AuthMethodsTabs.tsx @@ -122,7 +122,7 @@ const getHTTPSnippet = (authMethod: AuthMethod): string[] => { case "email": return [ `# Send a login code to the user -POST https://api.thirdweb.com/v1/wallets/user/code +POST https://api.thirdweb.com/v1/auth/initiate Content-Type: application/json x-client-id: @@ -131,7 +131,7 @@ x-client-id: "email": "user@example.com" }`, `# Verify the code and authenticate the user -POST https://api.thirdweb.com/v1/wallets/user/code/verify +POST https://api.thirdweb.com/v1/auth/complete Content-Type: application/json x-client-id: @@ -148,7 +148,7 @@ x-client-id: case "phone": return [ `# Send a login code to the user -POST https://api.thirdweb.com/v1/wallets/user/code +POST https://api.thirdweb.com/v1/auth/initiate Content-Type: application/json x-client-id: @@ -157,7 +157,7 @@ x-client-id: "phone": "+1234567890" }`, `# Verify the code and authenticate the user -POST https://api.thirdweb.com/v1/wallets/user/code/verify +POST https://api.thirdweb.com/v1/auth/complete Content-Type: application/json x-client-id: From 2cdaf2a3dc00688d1484e1fd1460c61c6966f35c Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 12:34:54 +1200 Subject: [PATCH 03/12] open api component improvements --- .../portal/src/app/wallets/get-users/page.mdx | 2 +- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 222 +++++++--------- .../APIEndpointMeta/OpenApiEndpoint.tsx | 90 ++----- .../Document/APIEndpointMeta/README.md | 250 ++++++++++++++++++ .../src/components/Document/Details.tsx | 2 +- 5 files changed, 378 insertions(+), 188 deletions(-) create mode 100644 apps/portal/src/components/Document/APIEndpointMeta/README.md diff --git a/apps/portal/src/app/wallets/get-users/page.mdx b/apps/portal/src/app/wallets/get-users/page.mdx index f4aa0180756..36bff7713f0 100644 --- a/apps/portal/src/app/wallets/get-users/page.mdx +++ b/apps/portal/src/app/wallets/get-users/page.mdx @@ -78,7 +78,7 @@ Required headers: ### Example - + diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index c14ed6709f1..84df4ec352a 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -3,34 +3,20 @@ import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; import { Details } from "../Details"; import { Heading } from "../Heading"; -import { Paragraph } from "../Paragraph"; import { RequestExample } from "./RequestExample"; -export type APIParameter = - | { - name: string; - required: false; - description: React.ReactNode; - type?: string; - example?: - | string - | boolean - | number - | object - | Array; - } - | { - name: string; - required: true; - example: - | string - | boolean - | number - | object - | Array; - description: React.ReactNode; - type?: string; - }; +export type APIParameter = { + name: string; + required: boolean; + description: React.ReactNode; + type?: string; + example?: + | string + | boolean + | number + | object + | Array; +}; export type ApiEndpointMeta = { title: string; @@ -98,33 +84,34 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { endpointUrl={props.metadata.path} method={props.metadata.method} /> -
-
- {request.headers.length > 0 && ( - - )} - - {request.pathParameters.length > 0 && ( - - )} - - {request.queryParameters.length > 0 && ( - - )} - - {request.bodyParameters.length > 0 && ( - - )} + {/* Parameters section inside the card */} +
+ {request.headers.length > 0 && ( + + )} + + {request.pathParameters.length > 0 && ( + + )} + + {request.queryParameters.length > 0 && ( + + )} + + {request.bodyParameters.length > 0 && ( + + )} +
@@ -174,84 +161,75 @@ function ParameterSection(props: { parameters: APIParameter[]; }) { return ( -
-
- {props.title} -
-
- {props.parameters - .sort((a, b) => { - if (a.required === b.required) { - return 0; - } - return a.required ? -1 : 1; - }) - .map((param) => ( - - ))} -
+
+
+ {props.title} + + {props.parameters.length} + +
+ } + accordionItemClassName="border-0 my-0" + accordionTriggerClassName="p-4 hover:bg-muted/50 transition-colors" + > +
+ {props.parameters + .sort((a, b) => { + if (a.required === b.required) { + return 0; + } + return a.required ? -1 : 1; + }) + .map((param) => ( + + ))} +
+
); } -function ParameterItem({ param }: { param: APIParameter }) { +function InlineParameterItem({ param }: { param: APIParameter }) { return ( -
- {param.name} - {param.type && ( - - {param.type} - - )} -
- } - tags={param.required ? ["Required"] : []} - > -
- {param.description && ( -
-
Description
- {param.description} -
- )} - +
+
+ + {param.name} + {param.type && ( -
-
Type
-
- -
-
+ + {param.type} + )} - - {param.example !== undefined && ( -
-
Example
-
- -
-
+ {param.required && ( + + Required + )}
- + + {param.description && ( +
{param.description}
+ )} + + {param.example !== undefined && ( +
+ Example: + +
+ )} +
); } @@ -335,7 +313,7 @@ function createFetchCommand(params: { metadata: ApiEndpointMeta }) { } if (Object.keys(bodyObj).length > 0) { - fetchOptions.body = JSON.stringify(bodyObj); + fetchOptions.body = bodyObj; } return `fetch('${url}', ${JSON.stringify(fetchOptions, null, 2)})`; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 672f66e30c6..6052ea50a4c 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -86,7 +86,7 @@ interface OpenApiEndpointProps { specUrl?: string; path: string; method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - requestBodyOverride?: Record; + requestBodyOverride?: Record; responseExampleOverride?: Record; } @@ -101,27 +101,7 @@ const fetchOpenApiSpec = cache(async (url: string): Promise => { return response.json(); }); -// Helper function to get example values based on type -function getExampleForType(type?: string): unknown { - switch (type) { - case "string": - return "string"; - case "number": - case "integer": - return 0; - case "boolean": - return true; - case "array": - return []; - case "object": - return {}; - default: - return undefined; - } -} - // Recursively generate example values from OpenAPI schema -// eslint-disable-next-line @typescript-eslint/no-explicit-any function generateExampleFromSchema(schema: any, visited = new Set()): any { // Prevent infinite recursion with circular references if (visited.has(schema)) { @@ -194,7 +174,6 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { return []; case "object": { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const obj: Record = {}; if (schema.properties) { @@ -241,7 +220,6 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { default: // Handle allOf, oneOf, anyOf if (schema.allOf && Array.isArray(schema.allOf)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const merged: Record = {}; for (const subSchema of schema.allOf) { const subExample = generateExampleFromSchema(subSchema, visited); @@ -285,7 +263,7 @@ function transformOpenApiToApiEndpointMeta( spec: OpenApiSpec, path: string, method: string, - requestBodyOverride?: Record, + requestBodyOverride?: Record, responseExampleOverride?: Record, ): ApiEndpointMeta { const pathItem = spec.paths[path]; @@ -314,10 +292,7 @@ function transformOpenApiToApiEndpointMeta( type: param.schema?.type || "string", required: param.required || false, example: - param.example || - param.schema?.example || - param.schema?.default || - getExampleForType(param.schema?.type), + param.example || param.schema?.example || param.schema?.default, } as APIParameter; switch (param.in) { @@ -353,53 +328,40 @@ function transformOpenApiToApiEndpointMeta( const content = operation.requestBody.content; const jsonContent = content["application/json"]; - if (jsonContent?.schema?.properties) { - const required = jsonContent.schema.required || []; + const example = + jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; + const required = jsonContent?.schema?.required || []; + + if (example) { + // If there's a global example use that + if (typeof example === "object" && example !== null) { + for (const [key, value] of Object.entries(example)) { + const apiParam: APIParameter = { + name: key, + description: + jsonContent?.schema?.properties?.[key]?.description || "", + required: required.includes(key), + example: value, + } as APIParameter; + bodyParameters.push(apiParam); + } + } + } else if (jsonContent?.schema?.properties) { for (const [propName, propSchema] of Object.entries( jsonContent.schema.properties, )) { - const prop = propSchema as { - type?: string; - description?: string; - example?: string | number | boolean | object; - format?: string; - enum?: (string | number | boolean)[]; - }; - - // Generate example if not provided - let example = prop.example; - if (example === undefined) { - example = generateExampleFromSchema(prop); - } - + const prop = propSchema as any; const apiParam: APIParameter = { name: propName, description: prop.description || "", - type: prop.type || "string", + type: prop.type, required: required.includes(propName), - example: example, + example: prop.example, } as APIParameter; bodyParameters.push(apiParam); } - } else { - // Fallback: try to generate from the schema example - const example = - jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; - - if (example && typeof example === "object" && example !== null) { - for (const [key, value] of Object.entries(example)) { - const apiParam: APIParameter = { - name: key, - description: "", - required: false, - example: value, - } as APIParameter; - - bodyParameters.push(apiParam); - } - } } } @@ -435,7 +397,7 @@ function transformOpenApiToApiEndpointMeta( return { title: operation.summary || `${method.toUpperCase()} ${path}`, - description: "", // Always empty since we don't want to show titles/descriptions + description: operation.description || operation.summary || "", path, origin: baseUrl, method: method.toUpperCase() as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", diff --git a/apps/portal/src/components/Document/APIEndpointMeta/README.md b/apps/portal/src/components/Document/APIEndpointMeta/README.md new file mode 100644 index 00000000000..77630108185 --- /dev/null +++ b/apps/portal/src/components/Document/APIEndpointMeta/README.md @@ -0,0 +1,250 @@ +# API Endpoint Components + +This directory contains components for rendering API endpoint documentation with integrated parameter sections and collapsible accordions. + +## Components + +### `ApiEndpoint` (Server Component) + +Renders API endpoint documentation with request/response examples and integrated parameter sections. Takes a manually constructed `ApiEndpointMeta` object. Uses the existing `Details` client component for collapsible parameter sections. + +### `OpenApiEndpoint` (Server Component) + +Server component that fetches and renders API endpoint documentation from an OpenAPI specification. Uses Next.js caching for 1-hour cache duration and includes built-in Suspense boundaries. + +## Usage + +### Using OpenApiEndpoint + +```tsx +import { OpenApiEndpoint } from "@/components/Document/APIEndpointMeta"; + +// Basic usage with built-in Suspense + + +// Using default thirdweb API spec + + +// With custom request body override + + +// With custom response example override + + +// With both overrides + +``` + +### Using ApiEndpoint (manual) + +```tsx +import { ApiEndpoint } from "@/components/Document/APIEndpointMeta"; + +const metadata = { + title: "Get User", + description: "Retrieve a user by ID", + path: "/users/{id}", + origin: "https://api.example.com", + method: "GET" as const, + request: { + pathParameters: [ + { + name: "id", + required: true, + description: "User ID", + example: "123", + }, + ], + headers: [], + queryParameters: [], + bodyParameters: [], + }, + responseExamples: { + "200": JSON.stringify({ id: "123", name: "John Doe" }, null, 2), + }, +}; + +; +``` + +## Features + +- **Server-side rendering**: `OpenApiEndpoint` is a server component for better performance +- **Next.js caching**: OpenAPI specs are cached for 1 hour using Next.js `fetch` cache +- **Built-in Suspense**: Automatic loading states with Suspense boundaries +- **Request examples**: Automatically generates fetch and curl examples +- **Smart response examples**: Recursively generates examples from OpenAPI schemas when no explicit examples exist +- **Parameter extraction**: Extracts path, header, query, and body parameters from OpenAPI specs +- **Integrated parameter display**: Parameters are shown within the Request card for better organization +- **Collapsible parameter sections**: Each parameter category (Headers, Path, Query, Body) is collapsible +- **Inline parameter view**: Parameters are displayed inline with clear visual hierarchy +- **Schema-aware generation**: Handles complex nested objects, arrays, and various data types +- **Circular reference protection**: Prevents infinite recursion in self-referencing schemas +- **Format-aware examples**: Generates appropriate examples for dates, emails, URLs, etc. +- **Custom overrides**: Override request body and response examples with custom data +- **Flexible customization**: Maintain OpenAPI structure while customizing specific examples +- **Error handling**: Graceful error handling for invalid specs or missing endpoints +- **TypeScript support**: Full TypeScript support with proper type definitions + +## UI/UX Improvements + +### Integrated Parameter Display + +Parameters are now displayed within the Request card, creating a more cohesive experience: + +- Code examples appear at the top +- Parameter sections appear below the code, within the same bordered card +- Creates a single, unified Request section + +### Collapsible Parameter Categories + +Each parameter type is now a collapsible section: + +- **Headers** - Authentication and request headers +- **Path Parameters** - URL path variables +- **Query Parameters** - URL query string parameters +- **Request Body** - POST/PUT request body parameters + +Each section shows: + +- Parameter count badge +- Expandable/collapsible chevron icon +- Hover effects for better interactivity + +### Inline Parameter Display + +When expanded, parameters are shown inline with: + +- Parameter name in monospace font +- Type badge (string, number, etc.) +- Required indicator for mandatory parameters +- Description and example values +- Clean, card-based layout with proper spacing + +## Custom Overrides + +The component supports two optional props for customizing examples: + +### `requestBodyOverride?: Record` + +Completely replaces the request body parameters extracted from the OpenAPI spec. Useful when you want to show specific examples that differ from the generic schema. + +```tsx +// Override with blockchain-specific examples + +``` + +### `responseExampleOverride?: Record` + +Replaces the response examples with custom JSON strings. The keys should be HTTP status codes. + +```tsx +// Override with custom response format + +``` + +## Smart Example Generation + +The `generateExampleFromSchema` function intelligently creates response examples by: + +1. **Using explicit examples** when available from the schema +2. **Handling all OpenAPI types**: string, number, integer, boolean, array, object, null +3. **Format-specific examples**: Generates realistic examples for date-time, email, URL formats +4. **Nested object support**: Recursively processes complex nested structures +5. **Array handling**: Creates arrays with appropriate item examples +6. **Schema composition**: Supports `allOf`, `oneOf`, `anyOf` schema patterns +7. **Circular reference detection**: Prevents infinite loops in self-referencing schemas +8. **Required vs optional properties**: Always includes required properties, selectively includes optional ones + +## Architecture + +- **`OpenApiEndpoint`**: Server component with built-in Suspense that fetches and transforms OpenAPI specs +- **`ApiEndpoint`**: Server component that displays the formatted endpoint documentation +- **`ParameterSection`**: Server component that wraps parameters in the existing `Details` component +- **`Details`**: Existing client component that provides collapsible accordion functionality +- **`InlineParameterItem`**: Inline parameter display with clean visual hierarchy +- **`generateExampleFromSchema`**: Recursive utility that creates realistic examples from OpenAPI schemas + +The server component approach provides better performance by: + +- Fetching data at build time or on the server +- Reducing client-side JavaScript bundle size +- Utilizing Next.js built-in caching mechanisms +- Enabling static generation when possible + +The `Details` client component provides interactive features: + +- Collapsible parameter sections with built-in accordion functionality +- Hover effects and animations +- State management for expanded/collapsed sections +- Anchor link support for deep linking diff --git a/apps/portal/src/components/Document/Details.tsx b/apps/portal/src/components/Document/Details.tsx index d803e7f8399..71be6640f8b 100644 --- a/apps/portal/src/components/Document/Details.tsx +++ b/apps/portal/src/components/Document/Details.tsx @@ -58,7 +58,7 @@ export function Details(props: { props.accordionTriggerClassName, )} > -
{props.children}
+
{props.children}
); } From a74e1a227804bef9405ce14170f311eee8e1f9ab Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 12:51:11 +1200 Subject: [PATCH 04/12] fixes --- apps/portal/src/app/contracts/deploy/page.mdx | 63 ++++++++++++------- apps/portal/src/app/contracts/page.mdx | 12 ++-- .../src/app/transactions/monitor/page.mdx | 8 +++ .../src/app/transactions/sponsor/page.mdx | 2 + apps/portal/src/app/wallets/server/page.mdx | 31 ++++++++- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 4 +- .../APIEndpointMeta/OpenApiEndpoint.tsx | 2 +- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/apps/portal/src/app/contracts/deploy/page.mdx b/apps/portal/src/app/contracts/deploy/page.mdx index 22aee32efe4..1da010b0d24 100644 --- a/apps/portal/src/app/contracts/deploy/page.mdx +++ b/apps/portal/src/app/contracts/deploy/page.mdx @@ -1,3 +1,14 @@ +import { OpenApiEndpoint, createMetadata } from "@doc"; + +export const metadata = createMetadata({ + image: { + title: "Deploy contracts", + icon: "contracts", + }, + title: "Deploy Contracts", + description: "Deploy contracts on any EVM chain.", +}); + # Deploy Contracts You can deploy contracts via CLI or programatically. @@ -16,8 +27,6 @@ You can also publish your contract to be deployable by anyone on any chain. npx thirdweb publish -k ``` - - ## Deploying via SDK You can deploy contracts from ABI and bytecode. @@ -26,15 +35,15 @@ You can deploy contracts from ABI and bytecode. import { deployContract } from "thirdweb/deploys"; const address = await deployContract({ - client, - chain, - bytecode: "0x...", - abi: contractAbi, - constructorParams: { - param1: "value1", - param2: 123, - }, - salt, // optional: salt enables deterministic deploys + client, + chain, + bytecode: "0x...", + abi: contractAbi, + constructorParams: { + param1: "value1", + param2: 123, + }, + salt, // optional: salt enables deterministic deploys }); ``` @@ -44,14 +53,26 @@ Or alternatively, you can deploy a contract from a previously published contract import { deployPublishedContract } from "thirdweb/deploys"; const address = await deployPublishedContract({ - client, - chain, - account, - contractId: "MyPublishedContract", - contractParams: { - param1: "value1", - param2: 123, - }, - publisher: "0x...", // optional, defaults to the thirdweb deployer + client, + chain, + account, + contractId: "MyPublishedContract", + contractParams: { + param1: "value1", + param2: 123, + }, + publisher: "0x...", // optional, defaults to the thirdweb deployer }); -``` \ No newline at end of file +``` + +## Deploy via API + +You can also deploy contracts via API by passing the contract bytecode and ABI. This will automatically verify the contract on block explorers and add it to your project dashboard. + + + +## List all deployed contracts + +You can list all deployed contracts for your project. + + diff --git a/apps/portal/src/app/contracts/page.mdx b/apps/portal/src/app/contracts/page.mdx index b5286e7a2a3..2bc003eb0a0 100644 --- a/apps/portal/src/app/contracts/page.mdx +++ b/apps/portal/src/app/contracts/page.mdx @@ -71,19 +71,15 @@ Read, write, and deploy smart contracts on any EVM compatible blockchain. ### Read from Contracts + You can efficiently read data from multiple functions or contracts in a single request. + ### Write to Contracts - - - ### Deploy Contracts + You can write multiple functions to multiple contracts on the same chain in a single request atomically, which will result in a single transaction for optimal gas efficiency. - - - ### List Contracts - - + diff --git a/apps/portal/src/app/transactions/monitor/page.mdx b/apps/portal/src/app/transactions/monitor/page.mdx index 2a3163a3914..32d13549b3e 100644 --- a/apps/portal/src/app/transactions/monitor/page.mdx +++ b/apps/portal/src/app/transactions/monitor/page.mdx @@ -39,7 +39,15 @@ Monitor and get notified about transactions in your application, both on your pr + ### Get transaction status + + Get the information about a transaction by its id. + + + ### List transactions + + List all transactions for your project with filtering and pagination. diff --git a/apps/portal/src/app/transactions/sponsor/page.mdx b/apps/portal/src/app/transactions/sponsor/page.mdx index 9a199dac30b..647753f9ecf 100644 --- a/apps/portal/src/app/transactions/sponsor/page.mdx +++ b/apps/portal/src/app/transactions/sponsor/page.mdx @@ -57,6 +57,8 @@ Sponsor gas fees using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), enab EIP-7702 is the default execution mode in the [thirdweb API](https://api.thirdweb.com/reference) for your user and server wallets. +Example sponsored contract write request: + diff --git a/apps/portal/src/app/wallets/server/page.mdx b/apps/portal/src/app/wallets/server/page.mdx index 523c525c607..a7631add06c 100644 --- a/apps/portal/src/app/wallets/server/page.mdx +++ b/apps/portal/src/app/wallets/server/page.mdx @@ -33,9 +33,38 @@ Server wallets are wallets that are managed by your own application, like a trea Once created, you can use your server wallet by passing it as the `from` field of the [thirdweb API](https://api.thirdweb.com/reference#tag/transactions/post/v1/transactions). +### Create a new Server Wallet + +You can create new server wallets via API by just passing an identifier. This can be any string, but we recommend using a descriptive name. + - +### List all Server Wallets + +You can also list all server wallets for your project to get their addresses and identifiers. + + diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index 84df4ec352a..8df3eb29690 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -62,7 +62,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
- + Request
@@ -116,7 +116,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
- + Response
diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 6052ea50a4c..8b971764877 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -370,7 +370,7 @@ function transformOpenApiToApiEndpointMeta( // Use override if provided, otherwise generate from OpenAPI spec if (responseExampleOverride) { - Object.assign(responseExamples, responseExampleOverride); + responseExamples["200"] = JSON.stringify(responseExampleOverride, null, 2); } else { for (const [statusCode, response] of Object.entries(operation.responses)) { const content = response.content; From c1993a649fb094aab3da98d190182049a12c07df Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 13:28:17 +1200 Subject: [PATCH 05/12] more fixes --- .../app/wallets/pregenerate-wallets/page.mdx | 2 +- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 39 +++++++++++++++++-- .../APIEndpointMeta/OpenApiEndpoint.tsx | 28 +++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx b/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx index 8698e3be511..7e7b0aa3545 100644 --- a/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx +++ b/apps/portal/src/app/wallets/pregenerate-wallets/page.mdx @@ -24,7 +24,7 @@ Create wallets for users before they authenticate, enabling smoother onboarding ## API Reference - + For ecosystem wallets, the secret key must be from the same account as the ecosystem owner. diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index 8df3eb29690..ca8aef882e5 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -247,8 +247,20 @@ function createCurlCommand(params: { metadata: ApiEndpointMeta }) { url += `?${queryParams}`; } - const headers = params.metadata.request.headers - .filter((h) => h.example !== undefined) + const displayHeaders = params.metadata.request.headers.filter( + (h) => h.example !== undefined, + ); + + // always show secret key header in examples + displayHeaders.push({ + name: "x-secret-key", + example: "", + description: + "Project secret key - for backend usage only. Should not be used in frontend code.", + required: false, + }); + + const headers = displayHeaders .map((h) => { return `-H "${h.name}:${ typeof h.example === "object" && h !== null @@ -290,7 +302,28 @@ function createFetchCommand(params: { metadata: ApiEndpointMeta }) { url += `?${queryParams}`; } - for (const param of request.headers) { + const displayHeaders = request.headers.filter((h) => h.example !== undefined); + + // always show secret key header in examples + if (params.metadata.path.includes("/v1/auth")) { + displayHeaders.push({ + name: "x-client-id", + example: "", + description: + "Project client ID - for frontend usage on authorized domains.", + required: false, + }); + } else { + displayHeaders.push({ + name: "x-secret-key", + example: "", + description: + "Project secret key - for backend usage only. Should not be used in frontend code.", + required: false, + }); + } + + for (const param of displayHeaders) { if (param.example !== undefined) { headersObj[param.name] = param.example; } diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 8b971764877..4c5c8cc858d 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -309,6 +309,34 @@ function transformOpenApiToApiEndpointMeta( } } + // push default headers hardcoded for now + headers.push({ + name: "x-secret-key", + type: "backend", + description: + "Project secret key - for backend usage only. Should not be used in frontend code.", + required: false, + example: undefined, + }); + headers.push({ + name: "x-client-id", + type: "frontend", + description: + "Project client ID - for frontend usage on authorized domains.", + required: false, + example: undefined, + }); + + if (method === "POST" && !path.includes("/v1/contracts/read")) { + headers.push({ + name: "Authorization", + type: "frontend", + description: "Bearer token (JWT) for user wallet authentication", + required: false, + example: undefined, + }); + } + // Transform request body parameters const bodyParameters: APIParameter[] = []; From 2365dd1fe21635a3463deb98ad34f46413e45a3f Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 13:34:12 +1200 Subject: [PATCH 06/12] more --- apps/portal/src/app/wallets/page.mdx | 9 ++++++--- .../Document/APIEndpointMeta/OpenApiEndpoint.tsx | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/portal/src/app/wallets/page.mdx b/apps/portal/src/app/wallets/page.mdx index 907e7a7d04c..c5b869da205 100644 --- a/apps/portal/src/app/wallets/page.mdx +++ b/apps/portal/src/app/wallets/page.mdx @@ -72,10 +72,13 @@ Create wallets for your users with flexible authentication options. Choose from ## API Authentication -Create and authenticate user wallets using different methods: +Authenticating a user is done in two steps: + +1. Initiate authentication +2. Complete authentication ### Initiate Authentication -Start authentication with email, phone, passkey, or social providers: +Start authentication with email, phone, passkey, or social providers @@ -94,7 +97,7 @@ Retrieve authenticated user's wallet details: ### Installation - Install the thirdweb SDK in your TypeScript project: + Install the thirdweb SDK in your TypeScript project Date: Tue, 12 Aug 2025 14:02:15 +1200 Subject: [PATCH 07/12] handle oneOf cases --- apps/portal/package.json | 1 + .../Document/APIEndpointMeta/ApiEndpoint.tsx | 167 ++++++++---- .../APIEndpointMeta/DynamicRequestExample.tsx | 206 +++++++++++++++ .../APIEndpointMeta/OpenApiEndpoint.tsx | 218 ++++++++------- .../Document/APIEndpointMeta/README.md | 250 ------------------ .../APIEndpointMeta/RequestExample.tsx | 163 ++++++++++-- .../Document/APIEndpointMeta/index.ts | 2 + pnpm-lock.yaml | 28 +- 8 files changed, 585 insertions(+), 450 deletions(-) create mode 100644 apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx delete mode 100644 apps/portal/src/components/Document/APIEndpointMeta/README.md diff --git a/apps/portal/package.json b/apps/portal/package.json index 3f014468ce6..ca4879b1b84 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -24,6 +24,7 @@ "nextjs-toploader": "^1.6.12", "node-html-markdown": "^1.3.0", "node-html-parser": "^6.1.13", + "openapi-types": "12.1.3", "posthog-js": "1.256.1", "prettier": "3.6.2", "react": "19.1.0", diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index ca8aef882e5..6d280a41800 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -3,6 +3,7 @@ import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; import { Details } from "../Details"; import { Heading } from "../Heading"; +import { DynamicRequestExample } from "./DynamicRequestExample"; import { RequestExample } from "./RequestExample"; export type APIParameter = { @@ -18,6 +19,11 @@ export type APIParameter = { | Array; }; +export type RequestExampleType = { + title: string; + bodyParameters: APIParameter[]; +}; + export type ApiEndpointMeta = { title: string; description: React.ReactNode; @@ -29,6 +35,8 @@ export type ApiEndpointMeta = { headers: APIParameter[]; queryParameters: APIParameter[]; bodyParameters: APIParameter[]; + // Support for multiple request examples (oneOf schemas) + requestExamples?: RequestExampleType[]; }; responseExamples: Record; }; @@ -36,26 +44,65 @@ export type ApiEndpointMeta = { export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { const { responseExamples, request } = props.metadata; + // If we have multiple request examples (oneOf schemas), create code examples for each const requestExamples: Array<{ lang: "javascript" | "bash"; code: string; label: string; - }> = [ - { - code: createFetchCommand({ - metadata: props.metadata, - }), - label: "Fetch", - lang: "javascript", - }, - { - code: createCurlCommand({ - metadata: props.metadata, - }), - label: "Curl", - lang: "bash", - }, - ]; + format: "fetch" | "curl"; + exampleType?: string; + bodyParameters?: APIParameter[]; + }> = []; + + if (request.requestExamples && request.requestExamples.length > 0) { + // Create examples for each oneOf schema + for (const requestExample of request.requestExamples) { + const metadataWithExample = { + ...props.metadata, + request: { + ...request, + bodyParameters: requestExample.bodyParameters, + }, + }; + + requestExamples.push( + { + code: createFetchCommand({ metadata: metadataWithExample }), + label: `Fetch - ${requestExample.title}`, + lang: "javascript", + format: "fetch", + exampleType: requestExample.title, + bodyParameters: requestExample.bodyParameters, + }, + { + code: createCurlCommand({ metadata: metadataWithExample }), + label: `Curl - ${requestExample.title}`, + lang: "bash", + format: "curl", + exampleType: requestExample.title, + bodyParameters: requestExample.bodyParameters, + }, + ); + } + } else { + // Default single example + requestExamples.push( + { + code: createFetchCommand({ metadata: props.metadata }), + label: "Fetch", + lang: "javascript", + format: "fetch", + bodyParameters: request.bodyParameters, + }, + { + code: createCurlCommand({ metadata: props.metadata }), + label: "Curl", + lang: "bash", + format: "curl", + bodyParameters: request.bodyParameters, + }, + ); + } const responseKeys = Object.keys(responseExamples); @@ -65,11 +112,22 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { Request -
- on server and pass it to client - codeExamples={requestExamples.map((example) => { - return { + + {/* Use DynamicRequestExample for multiple examples, regular for single */} + {request.requestExamples && request.requestExamples.length > 0 ? ( + + ) : ( +
+ ({ code: ( ), label: example.label, - }; - })} - endpointUrl={props.metadata.path} - method={props.metadata.method} - /> + }))} + endpointUrl={props.metadata.path} + method={props.metadata.method} + /> + + {/* Parameters section inside the card */} +
+ {request.headers.length > 0 && ( + + )} + + {request.pathParameters.length > 0 && ( + + )} - {/* Parameters section inside the card */} -
- {request.headers.length > 0 && ( - - )} - - {request.pathParameters.length > 0 && ( - - )} - - {request.queryParameters.length > 0 && ( - - )} - - {request.bodyParameters.length > 0 && ( - - )} + {request.queryParameters.length > 0 && ( + + )} + + {request.bodyParameters.length > 0 && ( + + )} +
-
+ )}
diff --git a/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx b/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx new file mode 100644 index 00000000000..c9932086f7d --- /dev/null +++ b/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import CodeClient, { CodeLoading } from "../../code/code.client"; +import { Details } from "../Details"; +import type { APIParameter } from "./ApiEndpoint"; +import { RequestExample } from "./RequestExample"; + +interface DynamicRequestExampleProps { + requestExamples: Array<{ + lang: "javascript" | "bash"; + code: string; + label: string; + format: "fetch" | "curl"; + exampleType?: string; + bodyParameters?: APIParameter[]; + }>; + endpointUrl: string; + method: string; + pathParameters: APIParameter[]; + headers: APIParameter[]; + queryParameters: APIParameter[]; + hasMultipleExamples: boolean; +} + +function InlineParameterItem({ param }: { param: APIParameter }) { + return ( +
+
+ + {param.name} + + {param.type && ( + + {param.type} + + )} + {param.required && ( + + Required + + )} +
+ + {param.description && ( +
{param.description}
+ )} + + {param.example !== undefined && ( +
+ Example: + } + scrollableContainerClassName="m-0" + scrollableClassName="max-h-[200px]" + /> +
+ )} +
+ ); +} + +function ParameterSection(props: { + title: string; + parameters: APIParameter[]; +}) { + if (props.parameters.length === 0) return null; + + return ( +
+
+ {props.title} + + {props.parameters.length} + +
+ } + accordionItemClassName="border-0 my-0" + accordionTriggerClassName="p-4 hover:bg-muted/50 transition-colors" + > +
+ {props.parameters + .sort((a, b) => { + if (a.required === b.required) { + return 0; + } + return a.required ? -1 : 1; + }) + .map((param) => ( + + ))} +
+ +
+ ); +} + +const queryClient = new QueryClient(); + +export function DynamicRequestExample(props: DynamicRequestExampleProps) { + const [selectedFormat, setSelectedFormat] = useState<"fetch" | "curl">( + "fetch", + ); + const [selectedExampleType, setSelectedExampleType] = useState(""); + + // Initialize selected example type from first available example + useEffect(() => { + if (props.requestExamples.length > 0) { + const firstExampleType = + props.requestExamples.find((ex) => ex.format === "fetch") + ?.exampleType || ""; + setSelectedExampleType(firstExampleType); + } + }, [props.requestExamples]); + + // Get the current example based on selected format and example type + const selectedExample = + props.requestExamples.find( + (ex) => + ex.format === selectedFormat && ex.exampleType === selectedExampleType, + ) || + props.requestExamples.find((ex) => ex.format === selectedFormat) || + props.requestExamples[0]; + + // Get the current body parameters based on selected example + const currentBodyParameters = selectedExample?.bodyParameters || []; + + return ( + +
+ ({ + code: ( + } + scrollableContainerClassName="m-0" + lang={example.lang} + /> + ), + label: example.label, + format: example.format, + exampleType: example.exampleType, + }))} + endpointUrl={props.endpointUrl} + method={props.method} + hasSeparateDropdowns={props.hasMultipleExamples} + selectedExample={ + selectedExample + ? { + code: ( + } + scrollableContainerClassName="m-0" + lang={selectedExample.lang} + /> + ), + label: selectedExample.label, + format: selectedExample.format, + exampleType: selectedExample.exampleType, + } + : undefined + } + selectedFormat={selectedFormat} + selectedExampleType={selectedExampleType} + onFormatChange={(format) => { + setSelectedFormat(format); + }} + onExampleTypeChange={(exampleType) => { + setSelectedExampleType(exampleType); + }} + /> + + {/* Parameters section inside the card */} +
+ + + + +
+
+
+ ); +} diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index ab4527104f9..96457238e03 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -1,3 +1,4 @@ +import type { OpenAPIV3_1 } from "openapi-types"; import { cache, Suspense } from "react"; import { type APIParameter, @@ -6,81 +7,7 @@ import { } from "./ApiEndpoint"; // OpenAPI 3.0 types (simplified for our needs) -interface OpenApiSpec { - openapi: string; - info: { - title: string; - version: string; - }; - servers?: Array<{ - url: string; - description?: string; - }>; - paths: { - [path: string]: { - [method: string]: { - summary?: string; - description?: string; - parameters?: Array<{ - name: string; - in: "query" | "header" | "path" | "cookie"; - required?: boolean; - description?: string; - schema?: { - type?: string; - example?: any; - default?: any; - }; - example?: any; - }>; - requestBody?: { - required?: boolean; - content: { - [mediaType: string]: { - schema?: { - type?: string; - properties?: { - [key: string]: { - type?: string; - description?: string; - example?: any; - required?: boolean; - }; - }; - required?: string[]; - example?: any; - examples?: any[]; - }; - }; - }; - }; - responses: { - [statusCode: string]: { - description: string; - content?: { - [mediaType: string]: { - schema?: { - type?: string; - properties?: { - [key: string]: { - type?: string; - description?: string; - example?: any; - required?: boolean; - }; - }; - required?: string[]; - example?: any; - examples?: any[]; - }; - }; - }; - }; - }; - }; - }; - }; -} +type OpenApiSpec = OpenAPIV3_1.Document; interface OpenApiEndpointProps { specUrl?: string; @@ -266,12 +193,14 @@ function transformOpenApiToApiEndpointMeta( requestBodyOverride?: Record, responseExampleOverride?: Record, ): ApiEndpointMeta { - const pathItem = spec.paths[path]; + const pathItem = spec.paths?.[path]; if (!pathItem) { throw new Error(`Path ${path} not found in OpenAPI spec`); } - const operation = pathItem[method.toLowerCase()]; + const operation = pathItem[ + method.toLowerCase() as keyof typeof pathItem + ] as OpenAPIV3_1.OperationObject; if (!operation) { throw new Error(`Method ${method} not found for path ${path}`); } @@ -285,14 +214,14 @@ function transformOpenApiToApiEndpointMeta( const queryParameters: APIParameter[] = []; if (operation.parameters) { - for (const param of operation.parameters) { + for (const param of operation.parameters as OpenAPIV3_1.ParameterObject[]) { + const schema = param.schema as OpenAPIV3_1.SchemaObject; const apiParam: APIParameter = { name: param.name, description: param.description || "", - type: param.schema?.type || "string", + type: schema?.type || "string", required: param.required || false, - example: - param.example || param.schema?.example || param.schema?.default, + example: param.example || schema?.example || schema?.default, } as APIParameter; switch (param.in) { @@ -341,7 +270,11 @@ function transformOpenApiToApiEndpointMeta( example: undefined, }); - if (method === "POST" && !path.includes("/v1/contracts/read")) { + if ( + method === "POST" && + !path.includes("/v1/contracts/read") && + !path.includes("/v1/auth") + ) { headers.push({ name: "Authorization", type: "frontend", @@ -353,6 +286,10 @@ function transformOpenApiToApiEndpointMeta( // Transform request body parameters const bodyParameters: APIParameter[] = []; + const requestExamples: Array<{ + title: string; + bodyParameters: APIParameter[]; + }> = []; // Use override if provided if (requestBodyOverride) { @@ -367,43 +304,94 @@ function transformOpenApiToApiEndpointMeta( bodyParameters.push(apiParam); } } else if (operation.requestBody) { - const content = operation.requestBody.content; + const content = (operation.requestBody as OpenAPIV3_1.RequestBodyObject) + .content; const jsonContent = content["application/json"]; + const schema = jsonContent?.schema as OpenAPIV3_1.SchemaObject; + + // Check if this is a oneOf schema (multiple possible request bodies) + if (schema?.oneOf && Array.isArray(schema.oneOf)) { + // Parse each oneOf schema into separate request examples + for (const [index, oneOfSchema] of schema.oneOf.entries()) { + const schema = oneOfSchema as any; + const title = schema.title || `Option ${index + 1}`; + const required = schema.required || []; + const schemaBodyParameters: APIParameter[] = []; + + // Use schema examples if available + const schemaExamples = schema.examples || []; + const schemaExample = schemaExamples[index]; + + if (schemaExample && typeof schemaExample === "object") { + // Use the provided example + for (const [key, value] of Object.entries(schemaExample)) { + const property = schema.properties?.[key]; + const apiParam: APIParameter = { + name: key, + description: property?.description || "", + type: property?.type, + required: required.includes(key), + example: value as any, + }; + schemaBodyParameters.push(apiParam); + } + } else if (schema.properties) { + // Generate from schema properties + for (const [propName, propSchema] of Object.entries( + schema.properties, + )) { + const prop = propSchema as any; + const apiParam: APIParameter = { + name: propName, + description: prop.description || "", + type: prop.type, + required: required.includes(propName), + example: generateExampleFromSchema(prop) as any, + }; + schemaBodyParameters.push(apiParam); + } + } - const example = - jsonContent?.schema?.example || jsonContent?.schema?.examples?.[0]; - const required = jsonContent?.schema?.required || []; - - if (example) { - // If there's a global example use that - if (typeof example === "object" && example !== null) { - for (const [key, value] of Object.entries(example)) { + requestExamples.push({ + title, + bodyParameters: schemaBodyParameters, + }); + } + } else { + // Single schema (not oneOf) + const example = schema?.example || schema?.examples?.[0]; + const required = schema?.required || []; + + if (example) { + // If there's a global example use that + if (typeof example === "object" && example !== null) { + for (const [key, value] of Object.entries(example)) { + const apiParam: APIParameter = { + name: key, + description: schema?.properties?.[key]?.description || "", + required: required.includes(key), + example: value, + } as APIParameter; + + bodyParameters.push(apiParam); + } + } + } else if (schema?.properties) { + for (const [propName, propSchema] of Object.entries( + schema.properties, + )) { + const prop = propSchema as any; const apiParam: APIParameter = { - name: key, - description: - jsonContent?.schema?.properties?.[key]?.description || "", - required: required.includes(key), - example: value, + name: propName, + description: prop.description || "", + type: prop.type, + required: required.includes(propName), + example: prop.example, } as APIParameter; bodyParameters.push(apiParam); } } - } else if (jsonContent?.schema?.properties) { - for (const [propName, propSchema] of Object.entries( - jsonContent.schema.properties, - )) { - const prop = propSchema as any; - const apiParam: APIParameter = { - name: propName, - description: prop.description || "", - type: prop.type, - required: required.includes(propName), - example: prop.example, - } as APIParameter; - - bodyParameters.push(apiParam); - } } } @@ -414,8 +402,10 @@ function transformOpenApiToApiEndpointMeta( if (responseExampleOverride) { responseExamples["200"] = JSON.stringify(responseExampleOverride, null, 2); } else { - for (const [statusCode, response] of Object.entries(operation.responses)) { - const content = response.content; + for (const [statusCode, response] of Object.entries( + (operation as OpenAPIV3_1.OperationObject).responses || {}, + )) { + const content = (response as OpenAPIV3_1.ResponseObject).content; let example = ""; if (content) { @@ -448,6 +438,8 @@ function transformOpenApiToApiEndpointMeta( headers, queryParameters, bodyParameters, + // Include request examples if we have oneOf schemas + requestExamples: requestExamples.length > 0 ? requestExamples : undefined, }, responseExamples, }; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/README.md b/apps/portal/src/components/Document/APIEndpointMeta/README.md deleted file mode 100644 index 77630108185..00000000000 --- a/apps/portal/src/components/Document/APIEndpointMeta/README.md +++ /dev/null @@ -1,250 +0,0 @@ -# API Endpoint Components - -This directory contains components for rendering API endpoint documentation with integrated parameter sections and collapsible accordions. - -## Components - -### `ApiEndpoint` (Server Component) - -Renders API endpoint documentation with request/response examples and integrated parameter sections. Takes a manually constructed `ApiEndpointMeta` object. Uses the existing `Details` client component for collapsible parameter sections. - -### `OpenApiEndpoint` (Server Component) - -Server component that fetches and renders API endpoint documentation from an OpenAPI specification. Uses Next.js caching for 1-hour cache duration and includes built-in Suspense boundaries. - -## Usage - -### Using OpenApiEndpoint - -```tsx -import { OpenApiEndpoint } from "@/components/Document/APIEndpointMeta"; - -// Basic usage with built-in Suspense - - -// Using default thirdweb API spec - - -// With custom request body override - - -// With custom response example override - - -// With both overrides - -``` - -### Using ApiEndpoint (manual) - -```tsx -import { ApiEndpoint } from "@/components/Document/APIEndpointMeta"; - -const metadata = { - title: "Get User", - description: "Retrieve a user by ID", - path: "/users/{id}", - origin: "https://api.example.com", - method: "GET" as const, - request: { - pathParameters: [ - { - name: "id", - required: true, - description: "User ID", - example: "123", - }, - ], - headers: [], - queryParameters: [], - bodyParameters: [], - }, - responseExamples: { - "200": JSON.stringify({ id: "123", name: "John Doe" }, null, 2), - }, -}; - -; -``` - -## Features - -- **Server-side rendering**: `OpenApiEndpoint` is a server component for better performance -- **Next.js caching**: OpenAPI specs are cached for 1 hour using Next.js `fetch` cache -- **Built-in Suspense**: Automatic loading states with Suspense boundaries -- **Request examples**: Automatically generates fetch and curl examples -- **Smart response examples**: Recursively generates examples from OpenAPI schemas when no explicit examples exist -- **Parameter extraction**: Extracts path, header, query, and body parameters from OpenAPI specs -- **Integrated parameter display**: Parameters are shown within the Request card for better organization -- **Collapsible parameter sections**: Each parameter category (Headers, Path, Query, Body) is collapsible -- **Inline parameter view**: Parameters are displayed inline with clear visual hierarchy -- **Schema-aware generation**: Handles complex nested objects, arrays, and various data types -- **Circular reference protection**: Prevents infinite recursion in self-referencing schemas -- **Format-aware examples**: Generates appropriate examples for dates, emails, URLs, etc. -- **Custom overrides**: Override request body and response examples with custom data -- **Flexible customization**: Maintain OpenAPI structure while customizing specific examples -- **Error handling**: Graceful error handling for invalid specs or missing endpoints -- **TypeScript support**: Full TypeScript support with proper type definitions - -## UI/UX Improvements - -### Integrated Parameter Display - -Parameters are now displayed within the Request card, creating a more cohesive experience: - -- Code examples appear at the top -- Parameter sections appear below the code, within the same bordered card -- Creates a single, unified Request section - -### Collapsible Parameter Categories - -Each parameter type is now a collapsible section: - -- **Headers** - Authentication and request headers -- **Path Parameters** - URL path variables -- **Query Parameters** - URL query string parameters -- **Request Body** - POST/PUT request body parameters - -Each section shows: - -- Parameter count badge -- Expandable/collapsible chevron icon -- Hover effects for better interactivity - -### Inline Parameter Display - -When expanded, parameters are shown inline with: - -- Parameter name in monospace font -- Type badge (string, number, etc.) -- Required indicator for mandatory parameters -- Description and example values -- Clean, card-based layout with proper spacing - -## Custom Overrides - -The component supports two optional props for customizing examples: - -### `requestBodyOverride?: Record` - -Completely replaces the request body parameters extracted from the OpenAPI spec. Useful when you want to show specific examples that differ from the generic schema. - -```tsx -// Override with blockchain-specific examples - -``` - -### `responseExampleOverride?: Record` - -Replaces the response examples with custom JSON strings. The keys should be HTTP status codes. - -```tsx -// Override with custom response format - -``` - -## Smart Example Generation - -The `generateExampleFromSchema` function intelligently creates response examples by: - -1. **Using explicit examples** when available from the schema -2. **Handling all OpenAPI types**: string, number, integer, boolean, array, object, null -3. **Format-specific examples**: Generates realistic examples for date-time, email, URL formats -4. **Nested object support**: Recursively processes complex nested structures -5. **Array handling**: Creates arrays with appropriate item examples -6. **Schema composition**: Supports `allOf`, `oneOf`, `anyOf` schema patterns -7. **Circular reference detection**: Prevents infinite loops in self-referencing schemas -8. **Required vs optional properties**: Always includes required properties, selectively includes optional ones - -## Architecture - -- **`OpenApiEndpoint`**: Server component with built-in Suspense that fetches and transforms OpenAPI specs -- **`ApiEndpoint`**: Server component that displays the formatted endpoint documentation -- **`ParameterSection`**: Server component that wraps parameters in the existing `Details` component -- **`Details`**: Existing client component that provides collapsible accordion functionality -- **`InlineParameterItem`**: Inline parameter display with clean visual hierarchy -- **`generateExampleFromSchema`**: Recursive utility that creates realistic examples from OpenAPI schemas - -The server component approach provides better performance by: - -- Fetching data at build time or on the server -- Reducing client-side JavaScript bundle size -- Utilizing Next.js built-in caching mechanisms -- Enabling static generation when possible - -The `Details` client component provides interactive features: - -- Collapsible parameter sections with built-in accordion functionality -- Hover effects and animations -- State management for expanded/collapsed sections -- Anchor link support for deep linking diff --git a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx index 988165a6662..035034046f9 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Select, @@ -14,11 +14,60 @@ export function RequestExample(props: { codeExamples: Array<{ label: string; code: React.ReactElement; + format?: "fetch" | "curl"; + exampleType?: string; }>; method: string; endpointUrl: string; + onExampleChange?: (label: string) => void; + onFormatChange?: (format: "fetch" | "curl") => void; + onExampleTypeChange?: (exampleType: string) => void; + hasSeparateDropdowns?: boolean; + selectedExample?: (typeof props.codeExamples)[0]; + selectedFormat?: "fetch" | "curl"; + selectedExampleType?: string; }) { - const [selectedExample, setSelectedExample] = useState(props.codeExamples[0]); + const [internalSelectedExample, setInternalSelectedExample] = useState( + props.codeExamples[0], + ); + const [internalSelectedFormat, setInternalSelectedFormat] = useState< + "fetch" | "curl" + >("fetch"); + const [internalSelectedExampleType, setInternalSelectedExampleType] = + useState(""); + + // Use props values if provided, otherwise use internal state + const selectedExample = props.selectedExample || internalSelectedExample; + const selectedFormat = props.selectedFormat || internalSelectedFormat; + const selectedExampleType = + props.selectedExampleType || internalSelectedExampleType; + + // Initialize selected example type from first example (only for internal state) + useEffect(() => { + if ( + props.hasSeparateDropdowns && + props.codeExamples.length > 0 && + !props.selectedExampleType + ) { + const firstExampleType = + props.codeExamples.find((ex) => ex.format === "fetch")?.exampleType || + ""; + setInternalSelectedExampleType(firstExampleType); + // Set the initial selected example based on format and example type + const initialExample = props.codeExamples.find( + (ex) => + ex.format === selectedFormat && ex.exampleType === firstExampleType, + ); + if (initialExample) { + setInternalSelectedExample(initialExample); + } + } + }, [ + props.hasSeparateDropdowns, + props.codeExamples, + selectedFormat, + props.selectedExampleType, + ]); return (
@@ -31,25 +80,97 @@ export function RequestExample(props: { {props.endpointUrl}
- + {props.hasSeparateDropdowns ? ( +
+ {/* Format Dropdown (Fetch/Curl) */} + + + {/* Example Type Dropdown */} + {(() => { + const exampleTypes = [ + ...new Set( + props.codeExamples + .map((ex) => ex.exampleType) + .filter(Boolean), + ), + ]; + return exampleTypes.length > 1 ? ( + + ) : null; + })()} +
+ ) : ( + + )}
{selectedExample?.code}
diff --git a/apps/portal/src/components/Document/APIEndpointMeta/index.ts b/apps/portal/src/components/Document/APIEndpointMeta/index.ts index 25a859a4a04..8dd509a52d9 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/index.ts +++ b/apps/portal/src/components/Document/APIEndpointMeta/index.ts @@ -2,5 +2,7 @@ export { type APIParameter, ApiEndpoint, type ApiEndpointMeta, + type RequestExampleType, } from "./ApiEndpoint"; +export { DynamicRequestExample } from "./DynamicRequestExample"; export { OpenApiEndpoint } from "./OpenApiEndpoint"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b37fe8a9f..5916aabcdcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,6 +793,9 @@ importers: node-html-parser: specifier: ^6.1.13 version: 6.1.13 + openapi-types: + specifier: 12.1.3 + version: 12.1.3 posthog-js: specifier: 1.256.1 version: 1.256.1 @@ -7282,7 +7285,6 @@ packages: '@walletconnect/modal@2.7.0': resolution: {integrity: sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw==} - deprecated: Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm '@walletconnect/react-native-compat@2.17.3': resolution: {integrity: sha512-lHKwXKoB0rdDH1ukxUx7o86xosWbttWIHYMZ8tgAQC1k9VH3CZZCoBcHOAAX8iBzyb0n0UP3/9zRrOcJE5nz7Q==} @@ -16745,7 +16747,7 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -17491,7 +17493,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -26797,7 +26799,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.7 @@ -28469,8 +28471,8 @@ snapshots: '@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.8.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) eslint-plugin-react: 7.37.5(eslint@8.57.0) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.0) @@ -28489,7 +28491,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@8.1.1) @@ -28500,7 +28502,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.10.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -28525,18 +28527,18 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.8.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -28547,7 +28549,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31316,7 +31318,7 @@ snapshots: '@babel/core': 7.28.0 '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.2 flow-enums-runtime: 0.0.6 metro: 0.81.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) metro-babel-transformer: 0.81.4 From 5d047da9881f9ec227f04797638d2268bdee91f0 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 14:34:02 +1200 Subject: [PATCH 08/12] links --- apps/playground-web/package.json | 2 +- .../src/app/wallets/custom-auth/page.mdx | 5 +++- .../portal/src/app/wallets/get-users/page.mdx | 26 ------------------- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 8 ++++-- .../APIEndpointMeta/DynamicRequestExample.tsx | 3 +++ .../APIEndpointMeta/OpenApiEndpoint.tsx | 11 +++++++- .../APIEndpointMeta/RequestExample.tsx | 7 ++++- .../src/components/Document/Details.tsx | 4 +-- pnpm-lock.yaml | 2 +- 9 files changed, 33 insertions(+), 35 deletions(-) diff --git a/apps/playground-web/package.json b/apps/playground-web/package.json index 1332c0a73da..f443911f821 100644 --- a/apps/playground-web/package.json +++ b/apps/playground-web/package.json @@ -22,7 +22,7 @@ "next": "15.3.5", "next-themes": "^0.4.6", "nextjs-toploader": "^1.6.12", - "openapi-types": "^12.1.3", + "openapi-types": "12.1.3", "posthog-js": "1.256.1", "prettier": "3.6.2", "react": "19.1.0", diff --git a/apps/portal/src/app/wallets/custom-auth/page.mdx b/apps/portal/src/app/wallets/custom-auth/page.mdx index 1802c909beb..e7eec24f415 100644 --- a/apps/portal/src/app/wallets/custom-auth/page.mdx +++ b/apps/portal/src/app/wallets/custom-auth/page.mdx @@ -68,7 +68,10 @@ You will be asked to enter the following values - +", +}}/> diff --git a/apps/portal/src/app/wallets/get-users/page.mdx b/apps/portal/src/app/wallets/get-users/page.mdx index 36bff7713f0..6fb35c8b225 100644 --- a/apps/portal/src/app/wallets/get-users/page.mdx +++ b/apps/portal/src/app/wallets/get-users/page.mdx @@ -20,7 +20,6 @@ From your backend, you can list all your users and fetch the details of any user Single User All Users - Create User @@ -82,31 +81,6 @@ Required headers: - - -## Create User Wallet (Pregeneration) - -Pregenerate a user wallet before authentication. This creates a wallet in advance that can be claimed later when the user authenticates. - -### Use Cases - -- Create wallets based on known email addresses or user IDs -- Pre-fund wallets with tokens or NFTs before users claim them -- Enable smoother onboarding experiences - -### Authentication - -Required headers: -- `x-secret-key`: Your secret key for authentication -- `x-ecosystem-id` (optional): Your ecosystem ID -- `x-ecosystem-partner-id` (optional): Your ecosystem partner ID - -### Example - - - - - ## SDK Integration diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index 6d280a41800..e6178111a82 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -1,4 +1,5 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ExternalLinkIcon } from "lucide-react"; import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; import { Details } from "../Details"; @@ -27,6 +28,7 @@ export type RequestExampleType = { export type ApiEndpointMeta = { title: string; description: React.ReactNode; + referenceUrl: string; path: string; origin: string; method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; @@ -109,7 +111,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
- + Request @@ -118,6 +120,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { @@ -177,7 +181,7 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
- + Response
diff --git a/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx b/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx index c9932086f7d..5ae508c515c 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/DynamicRequestExample.tsx @@ -17,6 +17,7 @@ interface DynamicRequestExampleProps { bodyParameters?: APIParameter[]; }>; endpointUrl: string; + referenceUrl: string; method: string; pathParameters: APIParameter[]; headers: APIParameter[]; @@ -77,6 +78,7 @@ function ParameterSection(props: { return (
{props.title} @@ -154,6 +156,7 @@ export function DynamicRequestExample(props: DynamicRequestExampleProps) { exampleType: example.exampleType, }))} endpointUrl={props.endpointUrl} + referenceUrl={props.referenceUrl} method={props.method} hasSeparateDropdowns={props.hasMultipleExamples} selectedExample={ diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 96457238e03..5d5105af168 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -427,12 +427,15 @@ function transformOpenApiToApiEndpointMeta( } } + const tag = operation.tags?.[0] || ""; + return { title: operation.summary || `${method.toUpperCase()} ${path}`, description: operation.description || operation.summary || "", path, origin: baseUrl, method: method.toUpperCase() as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + referenceUrl: generateReferenceUrl(tag, path, method), request: { pathParameters, headers, @@ -479,7 +482,7 @@ async function OpenApiEndpointInner({ requestBodyOverride, responseExampleOverride, ); - return ; + return ; } catch (error) { return (
@@ -491,3 +494,9 @@ async function OpenApiEndpointInner({ ); } } + +const BASE_API_URL = "https://api.thirdweb.com"; + +function generateReferenceUrl(tag: string, path: string, method: string): string { + return `${BASE_API_URL}/reference#tag/${tag.toLowerCase()}/${method.toLowerCase()}${path}`; +} \ No newline at end of file diff --git a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx index 035034046f9..cd62855fb1f 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx @@ -9,6 +9,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { ExternalLinkIcon } from "lucide-react"; export function RequestExample(props: { codeExamples: Array<{ @@ -19,6 +20,7 @@ export function RequestExample(props: { }>; method: string; endpointUrl: string; + referenceUrl: string; onExampleChange?: (label: string) => void; onFormatChange?: (format: "fetch" | "curl") => void; onExampleTypeChange?: (exampleType: string) => void; @@ -77,7 +79,10 @@ export function RequestExample(props: { {props.method} - {props.endpointUrl} + + {props.endpointUrl} + +
{props.hasSeparateDropdowns ? ( diff --git a/apps/portal/src/components/Document/Details.tsx b/apps/portal/src/components/Document/Details.tsx index 71be6640f8b..9fb6e1b55c3 100644 --- a/apps/portal/src/components/Document/Details.tsx +++ b/apps/portal/src/components/Document/Details.tsx @@ -28,7 +28,7 @@ export function Details(props: { defaultOpen={props.startExpanded} trigger={
-

{props.summary} -

+ {props.tags && props.tags.length > 0 && (
{props.tags?.map((flag) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5916aabcdcb..a47e78e8cf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -637,7 +637,7 @@ importers: specifier: ^1.6.12 version: 1.6.12(next@15.3.5(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) openapi-types: - specifier: ^12.1.3 + specifier: 12.1.3 version: 12.1.3 posthog-js: specifier: 1.256.1 From 44d988c8824881f8c36999df747c47214e630766 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 14:36:53 +1200 Subject: [PATCH 09/12] enums --- .../Document/APIEndpointMeta/OpenApiEndpoint.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index 5d5105af168..e9bdf8003c0 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -64,7 +64,8 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { switch (schema.type) { case "string": if (schema.enum && schema.enum.length > 0) { - return schema.enum[0]; + // Join all enum values with | separator for display + return schema.enum.join(" | "); } if (schema.format === "date-time") { return new Date().toISOString(); @@ -82,6 +83,10 @@ function generateExampleFromSchema(schema: any, visited = new Set()): any { case "number": case "integer": + if (schema.enum && schema.enum.length > 0) { + // Join all enum values with | separator for display + return schema.enum.join(" | "); + } if (schema.minimum !== undefined) { return schema.minimum; } @@ -386,7 +391,7 @@ function transformOpenApiToApiEndpointMeta( description: prop.description || "", type: prop.type, required: required.includes(propName), - example: prop.example, + example: prop.example || generateExampleFromSchema(prop), } as APIParameter; bodyParameters.push(apiParam); From 61a12fb07666e057d534b3b5e5586fe8b7aeffa0 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 14:37:27 +1200 Subject: [PATCH 10/12] lint --- .../Document/APIEndpointMeta/ApiEndpoint.tsx | 13 ++++++++++--- .../Document/APIEndpointMeta/OpenApiEndpoint.tsx | 8 ++++++-- .../Document/APIEndpointMeta/RequestExample.tsx | 9 +++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index e6178111a82..e3905c166d6 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -1,5 +1,4 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { ExternalLinkIcon } from "lucide-react"; import { cn } from "../../../lib/utils"; import { CodeBlock } from "../Code"; import { Details } from "../Details"; @@ -111,7 +110,11 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) { return (
- + Request @@ -181,7 +184,11 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
- + Response
diff --git a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx index e9bdf8003c0..7a57f6dad94 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/OpenApiEndpoint.tsx @@ -502,6 +502,10 @@ async function OpenApiEndpointInner({ const BASE_API_URL = "https://api.thirdweb.com"; -function generateReferenceUrl(tag: string, path: string, method: string): string { +function generateReferenceUrl( + tag: string, + path: string, + method: string, +): string { return `${BASE_API_URL}/reference#tag/${tag.toLowerCase()}/${method.toLowerCase()}${path}`; -} \ No newline at end of file +} diff --git a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx index cd62855fb1f..d3cb2bd0447 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/RequestExample.tsx @@ -1,5 +1,6 @@ "use client"; +import { ExternalLinkIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { @@ -9,7 +10,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { ExternalLinkIcon } from "lucide-react"; export function RequestExample(props: { codeExamples: Array<{ @@ -79,7 +79,12 @@ export function RequestExample(props: { {props.method} - + {props.endpointUrl} From ea3cb7d277a2f6e9092915f8ca3c1ec06f5e1272 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 14:51:08 +1200 Subject: [PATCH 11/12] lint --- .../components/Document/APIEndpointMeta/ApiEndpoint.tsx | 2 +- .../src/components/Document/APIEndpointMeta/common.tsx | 9 --------- .../src/components/Document/APIEndpointMeta/index.ts | 8 -------- apps/portal/src/components/Document/index.ts | 1 - 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 apps/portal/src/components/Document/APIEndpointMeta/common.tsx delete mode 100644 apps/portal/src/components/Document/APIEndpointMeta/index.ts diff --git a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx index e3905c166d6..6c1f295e0b7 100644 --- a/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx +++ b/apps/portal/src/components/Document/APIEndpointMeta/ApiEndpoint.tsx @@ -19,7 +19,7 @@ export type APIParameter = { | Array; }; -export type RequestExampleType = { +type RequestExampleType = { title: string; bodyParameters: APIParameter[]; }; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/common.tsx b/apps/portal/src/components/Document/APIEndpointMeta/common.tsx deleted file mode 100644 index 799d67a0363..00000000000 --- a/apps/portal/src/components/Document/APIEndpointMeta/common.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { APIParameter } from "./ApiEndpoint"; - -export const secretKeyHeaderParameter: APIParameter = { - description: "Your project secret key for authentication.", - example: "", - name: "x-secret-key", - required: true, - type: "string", -}; diff --git a/apps/portal/src/components/Document/APIEndpointMeta/index.ts b/apps/portal/src/components/Document/APIEndpointMeta/index.ts deleted file mode 100644 index 8dd509a52d9..00000000000 --- a/apps/portal/src/components/Document/APIEndpointMeta/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - type APIParameter, - ApiEndpoint, - type ApiEndpointMeta, - type RequestExampleType, -} from "./ApiEndpoint"; -export { DynamicRequestExample } from "./DynamicRequestExample"; -export { OpenApiEndpoint } from "./OpenApiEndpoint"; diff --git a/apps/portal/src/components/Document/index.ts b/apps/portal/src/components/Document/index.ts index a928dee6e36..63e2dd9b22a 100644 --- a/apps/portal/src/components/Document/index.ts +++ b/apps/portal/src/components/Document/index.ts @@ -3,7 +3,6 @@ export { Badge } from "../ui/badge"; // export { Table } from "./Table"; export { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; -export { ApiEndpoint } from "./APIEndpointMeta/ApiEndpoint"; export { OpenApiEndpoint } from "./APIEndpointMeta/OpenApiEndpoint"; export { AuthList } from "./AuthList"; export { Breadcrumb } from "./Breadcrumb"; From d6c40f92a63cdb69d8cbf2c8b83a6d86e65300e4 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 12 Aug 2025 15:33:41 +1200 Subject: [PATCH 12/12] build --- .../src/app/nebula/api-reference/chat/EndpointMetadata.tsx | 2 ++ .../nebula/api-reference/clear-session/EndpointMetadata.tsx | 3 +++ .../nebula/api-reference/create-session/EndpointMetadata.tsx | 3 +++ .../nebula/api-reference/delete-session/EndpointMetadata.tsx | 3 +++ .../src/app/nebula/api-reference/execute/EndpointMetadata.tsx | 2 ++ .../app/nebula/api-reference/get-session/EndpointMetadata.tsx | 3 +++ .../app/nebula/api-reference/list-session/EndpointMetadata.tsx | 3 +++ .../nebula/api-reference/update-session/EndpointMetadata.tsx | 3 +++ 8 files changed, 22 insertions(+) diff --git a/apps/portal/src/app/nebula/api-reference/chat/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/chat/EndpointMetadata.tsx index c2f0856262c..d69c52f7dc8 100644 --- a/apps/portal/src/app/nebula/api-reference/chat/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/chat/EndpointMetadata.tsx @@ -30,7 +30,9 @@ export function EndpointMetadata() { method: "POST", origin: "https://nebula-api.thirdweb.com", path: "/chat", + referenceUrl: "https://api.thirdweb.com/reference#tag/ai/ai/chat", request: { + queryParameters: [], bodyParameters: [ { description: "The message to be processed.", diff --git a/apps/portal/src/app/nebula/api-reference/clear-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/clear-session/EndpointMetadata.tsx index 4af16becf6c..d1a6339905a 100644 --- a/apps/portal/src/app/nebula/api-reference/clear-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/clear-session/EndpointMetadata.tsx @@ -15,7 +15,10 @@ export function EndpointMetadata() { method: "POST", origin: "https://nebula-api.thirdweb.com", path: "/session/{session_id}/clear", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/clear-session", request: { + queryParameters: [], bodyParameters: [], headers: [nebulaSecretKeyHeaderParameter], pathParameters: [nebulaSessionIdPathParameter], diff --git a/apps/portal/src/app/nebula/api-reference/create-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/create-session/EndpointMetadata.tsx index 4772585a374..a56fc3db122 100644 --- a/apps/portal/src/app/nebula/api-reference/create-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/create-session/EndpointMetadata.tsx @@ -15,7 +15,10 @@ export function EndpointMetadata() { method: "POST", origin: "https://nebula-api.thirdweb.com", path: "/session", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/create-session", request: { + queryParameters: [], bodyParameters: [ { description: "Set a custom title for the session.", diff --git a/apps/portal/src/app/nebula/api-reference/delete-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/delete-session/EndpointMetadata.tsx index 0e02d2b846d..81e4f6784b9 100644 --- a/apps/portal/src/app/nebula/api-reference/delete-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/delete-session/EndpointMetadata.tsx @@ -22,7 +22,10 @@ export function EndpointMetadata() { method: "DELETE", origin: "https://nebula-api.thirdweb.com", path: "/session/{session_id}", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/delete-session", request: { + queryParameters: [], bodyParameters: [], headers: [nebulaSecretKeyHeaderParameter], pathParameters: [nebulaSessionIdPathParameter], diff --git a/apps/portal/src/app/nebula/api-reference/execute/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/execute/EndpointMetadata.tsx index 9af5d5aa1ed..93631e5c1b5 100644 --- a/apps/portal/src/app/nebula/api-reference/execute/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/execute/EndpointMetadata.tsx @@ -36,7 +36,9 @@ export function EndpointMetadata() { method: "POST", origin: "https://nebula-api.thirdweb.com", path: "/execute", + referenceUrl: "https://api.thirdweb.com/reference#tag/ai/ai/execute", request: { + queryParameters: [], bodyParameters: [ { description: "The message to be processed.", diff --git a/apps/portal/src/app/nebula/api-reference/get-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/get-session/EndpointMetadata.tsx index 2acd77f5132..f82ce213e0a 100644 --- a/apps/portal/src/app/nebula/api-reference/get-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/get-session/EndpointMetadata.tsx @@ -15,10 +15,13 @@ export function EndpointMetadata() { method: "GET", origin: "https://nebula-api.thirdweb.com", path: "/session/{session_id}", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/get-session", request: { bodyParameters: [], headers: [nebulaSecretKeyHeaderParameter], pathParameters: [nebulaSessionIdPathParameter], + queryParameters: [], }, responseExamples: { 200: nebulaFullSessionResponse, diff --git a/apps/portal/src/app/nebula/api-reference/list-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/list-session/EndpointMetadata.tsx index 2b7ac71f436..da97738c965 100644 --- a/apps/portal/src/app/nebula/api-reference/list-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/list-session/EndpointMetadata.tsx @@ -24,10 +24,13 @@ export function EndpointMetadata() { method: "GET", origin: "https://nebula-api.thirdweb.com", path: "/session/list", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/list-session", request: { bodyParameters: [], headers: [nebulaSecretKeyHeaderParameter], pathParameters: [], + queryParameters: [], }, responseExamples: { 200: response200Example, diff --git a/apps/portal/src/app/nebula/api-reference/update-session/EndpointMetadata.tsx b/apps/portal/src/app/nebula/api-reference/update-session/EndpointMetadata.tsx index 9c954c1f4ab..67e875ced33 100644 --- a/apps/portal/src/app/nebula/api-reference/update-session/EndpointMetadata.tsx +++ b/apps/portal/src/app/nebula/api-reference/update-session/EndpointMetadata.tsx @@ -16,6 +16,8 @@ export function EndpointMetadata() { method: "PUT", origin: "https://nebula-api.thirdweb.com", path: "/session/{session_id}", + referenceUrl: + "https://api.thirdweb.com/reference#tag/ai/ai/update-session", request: { bodyParameters: [ { @@ -28,6 +30,7 @@ export function EndpointMetadata() { ], headers: [nebulaSecretKeyHeaderParameter], pathParameters: [nebulaSessionIdPathParameter], + queryParameters: [], }, responseExamples: { 200: nebulaFullSessionResponse,