Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8c95a325 | ||
|
|
cf02cbf3af | ||
|
|
f152139d73 | ||
|
|
16307019be | ||
|
|
10fb338877 | ||
|
|
95412c75b9 | ||
|
|
7bc513c770 | ||
|
|
9069351e1a | ||
|
|
8f6363f4e4 | ||
|
|
a83104efff | ||
|
|
7936ebb6e9 | ||
|
|
3b986d52e5 | ||
|
|
ee4e13e098 | ||
|
|
8dc9e4102d | ||
|
|
33fc3e5226 | ||
|
|
ed47e93f1d | ||
|
|
1f7cda6698 |
@@ -592,7 +592,7 @@ const cached_getUniversalBridgeUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -636,7 +636,7 @@ const cached_getUniversalBridgeWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
|
||||
includeOwners: true,
|
||||
start: currentPage * pageSize,
|
||||
tokenByIndex,
|
||||
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -215,7 +215,7 @@ export function ServerWalletsTableUI({
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage > 1 ? currentPage - 1 : 1
|
||||
}`}
|
||||
legacyBehavior
|
||||
@@ -232,7 +232,7 @@ export function ServerWalletsTableUI({
|
||||
(pageNumber) => (
|
||||
<PaginationItem key={`page-${pageNumber}`}>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationLink isActive={currentPage === pageNumber}>
|
||||
@@ -244,7 +244,7 @@ export function ServerWalletsTableUI({
|
||||
)}
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||
}`}
|
||||
passHref
|
||||
|
||||
@@ -5,6 +5,6 @@ export const maxDuration = 300;
|
||||
export async function GET(_req: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Congratulations! You have accessed the protected route.",
|
||||
message: "Payment successful. You have accessed the protected route.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// const chain = arbitrumSepolia;
|
||||
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import { getDefaultToken } from "thirdweb/react";
|
||||
|
||||
export const chain = arbitrumSepolia;
|
||||
export const token = getDefaultToken(chain, "USDC")!;
|
||||
// export const chain = base;
|
||||
// export const token = {
|
||||
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
|
||||
// decimals: 18,
|
||||
// name: "Higher",
|
||||
// symbol: "HIGHER",
|
||||
// version: "1",
|
||||
// };
|
||||
// export const token = {
|
||||
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
|
||||
// decimals: 18,
|
||||
// name: "Stable Coin",
|
||||
// symbol: "SBC",
|
||||
// version: "1",
|
||||
// // primaryType: "Permit",
|
||||
// }
|
||||
// export const chain = defineChain(3338);
|
||||
// export const token = {
|
||||
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
|
||||
// decimals: 6,
|
||||
// name: "USDC",
|
||||
// symbol: "USDC",
|
||||
// version: "2",
|
||||
// }
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import {
|
||||
ConnectButton,
|
||||
getDefaultToken,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
@@ -14,9 +13,7 @@ import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { THIRDWEB_CLIENT } from "../../../../lib/client";
|
||||
|
||||
const chain = arbitrumSepolia;
|
||||
const token = getDefaultToken(chain, "USDC");
|
||||
import { chain, token } from "./constants";
|
||||
|
||||
export function X402ClientPreview() {
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -30,8 +27,21 @@ export function X402ClientPreview() {
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
BigInt(1 * 10 ** 18),
|
||||
);
|
||||
const response = await fetchWithPay("/api/paywall");
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("chainId", chain.id.toString());
|
||||
searchParams.set("payTo", activeWallet.getAccount()?.address || "");
|
||||
// TODO (402): dynamic from playground config
|
||||
// if (token) {
|
||||
// searchParams.set("amount", "0.01");
|
||||
// searchParams.set("tokenAddress", token.address);
|
||||
// searchParams.set("decimals", token.decimals.toString());
|
||||
// }
|
||||
const url =
|
||||
"/api/paywall" +
|
||||
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
|
||||
const response = await fetchWithPay(url.toString());
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
@@ -47,18 +57,20 @@ export function X402ClientPreview() {
|
||||
chain={chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken: {
|
||||
[chain.id]: token!.address,
|
||||
[chain.id]: token.address,
|
||||
},
|
||||
}}
|
||||
supportedTokens={{
|
||||
[chain.id]: [token!],
|
||||
[chain.id]: [token],
|
||||
}}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LockIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">Paid API Call</span>
|
||||
<span className="text-xl font-bold text-red-600">$0.01</span>
|
||||
<Badge variant="success">
|
||||
<span className="text-xl font-bold">0.1 {token.symbol}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -67,19 +79,25 @@ export function X402ClientPreview() {
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Pay Now
|
||||
Access Premium Content
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={"https://faucet.circle.com/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Click here to get USDC on {chain.name}
|
||||
</a>
|
||||
Pay for access with {token.symbol} on{" "}
|
||||
{chain.name || `chain ${chain.id}`}
|
||||
</p>
|
||||
{chain.testnet && token.symbol.toLowerCase() === "usdc" && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={"https://faucet.circle.com/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Click here to get testnet {token.symbol} on {chain.name}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
|
||||
@@ -59,6 +59,7 @@ function ServerCodeExample() {
|
||||
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
const thirdwebX402Facilitator = facilitator({
|
||||
@@ -76,7 +77,7 @@ export async function middleware(request: NextRequest) {
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0xYourWalletAddress",
|
||||
network: "eip155:11155111", // or any other chain id
|
||||
network: arbitrumSepolia, // or any other chain
|
||||
price: "$0.01", // can also be a ERC20 token amount
|
||||
facilitator: thirdwebX402Facilitator,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
import { createThirdwebClient, defineChain } from "thirdweb";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
|
||||
});
|
||||
|
||||
const chain = arbitrumSepolia;
|
||||
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
|
||||
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
|
||||
const ENGINE_VAULT_ACCESS_TOKEN = process.env
|
||||
.ENGINE_VAULT_ACCESS_TOKEN as string;
|
||||
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
|
||||
const twFacilitator = facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
@@ -25,14 +23,41 @@ export async function middleware(request: NextRequest) {
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
|
||||
const paymentData = request.headers.get("X-PAYMENT");
|
||||
const queryParams = request.nextUrl.searchParams;
|
||||
|
||||
const chainId = queryParams.get("chainId");
|
||||
const payTo = queryParams.get("payTo");
|
||||
|
||||
if (!chainId || !payTo) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required parameters" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO (402): dynamic from playground config
|
||||
// const amount = queryParams.get("amount");
|
||||
// const tokenAddress = queryParams.get("tokenAddress");
|
||||
// const decimals = queryParams.get("decimals");
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
network: `eip155:${chain.id}`,
|
||||
payTo: payTo as `0x${string}`,
|
||||
network: defineChain(Number(chainId)),
|
||||
price: "$0.01",
|
||||
// price: {
|
||||
// amount: toUnits(amount as string, parseInt(decimals as string)).toString(),
|
||||
// asset: {
|
||||
// address: tokenAddress as `0x${string}`,
|
||||
// decimals: decimals ? parseInt(decimals) : token.decimals,
|
||||
// eip712: {
|
||||
// name: token.name,
|
||||
// version: token.version,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"geist": "^1.5.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
|
||||
@@ -213,133 +213,129 @@ export function Header() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="flex w-full flex-col gap-2 border-b bg-background p-4 xl:pb-0 lg:px-8 overflow-hidden">
|
||||
{/* Top row */}
|
||||
<div className="container flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden xl:block">
|
||||
<header className="flex w-full border-b overflow-hidden relative">
|
||||
<div className="container flex-col">
|
||||
{/* Top row */}
|
||||
<div className="flex items-center justify-between gap-6 relative z-10 py-4 lg:pt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden xl:block">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="container hidden items-center justify-between gap-6 xl:flex mt-1">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center py-2 relative px-2.5 hover:text-foreground"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink href={link.href} name={link.name} />
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-violet-700 h-[2px] inset-x-0 rounded-full absolute -bottom-1" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="hidden items-start justify-between gap-6 xl:flex relative z-10">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1.5">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center relative"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
name={link.name}
|
||||
className="py-2.5 px-3 hover:bg-accent rounded-lg hover:text-foreground font-normal"
|
||||
/>
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-foreground h-[2px] inset-x-0 rounded-full absolute -bottom-1.5" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="px-1">
|
||||
<div className="flex items-center">
|
||||
<DropdownLinks
|
||||
category="AI"
|
||||
links={aiLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="SDKs"
|
||||
links={sdkLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="APIs"
|
||||
links={apisLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Tools"
|
||||
links={toolLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Support"
|
||||
links={supportLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
className="px-3 py-2.5 hover:bg-accent hover:text-foreground rounded-lg"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,6 +424,8 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-card/70 backdrop-blur-xl z-0" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -448,31 +446,29 @@ function DropdownLinks(props: {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="inline-flex items-center gap-1 p-0 font-medium text-muted-foreground text-sm hover:bg-transparent hover:text-foreground"
|
||||
className="inline-flex items-center gap-1.5 font-normal text-muted-foreground text-sm hover:bg-accent hover:text-foreground rounded-lg px-3 py-2.5"
|
||||
variant="ghost"
|
||||
>
|
||||
{props.category}
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground opacity-70" />
|
||||
<ChevronDownIcon className="size-3.5 text-muted-foreground opacity-70" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 bg-card p-3"
|
||||
style={{
|
||||
minWidth: "150px",
|
||||
}}
|
||||
className="flex flex-col gap-1 bg-card p-1 rounded-xl min-w-[200px]"
|
||||
sideOffset={14}
|
||||
>
|
||||
{props.links.map((info) => {
|
||||
return (
|
||||
<DropdownMenuItem asChild key={info.name}>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative flex cursor-pointer gap-2 font-medium text-foreground",
|
||||
"relative flex cursor-pointer gap-3 font-medium text-foreground !rounded-lg px-3 py-2",
|
||||
"hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{info.icon && (
|
||||
<info.icon className="size-5 text-foreground" />
|
||||
<info.icon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
@@ -524,6 +520,7 @@ function NavLink(props: {
|
||||
name: string;
|
||||
onClick?: () => void;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -534,6 +531,7 @@ function NavLink(props: {
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground",
|
||||
props.icon ? "flex flex-row gap-3" : "",
|
||||
props.className,
|
||||
)}
|
||||
href={props.href}
|
||||
onClick={props.onClick}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@@ -0,0 +1,467 @@
|
||||
import {
|
||||
Details,
|
||||
createMetadata,
|
||||
DocImage
|
||||
} from "@doc";
|
||||
import bridgeWidgetDark from "./bridge-widget-dark.png";
|
||||
import bridgeWidgetLight from "./bridge-widget-light.png";
|
||||
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "BridgeWidget Script",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Bridge Widget",
|
||||
description: "Add a widget to add cross swaps and fiat onramp to your app",
|
||||
});
|
||||
|
||||
# BridgeWidget Script
|
||||
|
||||
The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp UI into your app. Just add a script tag to your HTML and get a fully customizable widget — no build setup required.
|
||||
|
||||
## Key Features
|
||||
- Cross-chain token swaps across 80+ blockchains
|
||||
- Fiat onramp support to buy tokens with credit/debit cards
|
||||
- Customizable UI - use prebuilt themes or override with your brand colors
|
||||
- Prefill token selections for a smoother user experience
|
||||
- Display fiat values in multiple currencies
|
||||
- Event callbacks to track user actions (success, error, cancel, disconnect)
|
||||
|
||||
|
||||
<div className='dark-only'>
|
||||
<DocImage src={bridgeWidgetDark} />
|
||||
</div>
|
||||
<div className='light-only'>
|
||||
<DocImage src={bridgeWidgetLight} />
|
||||
</div>
|
||||
|
||||
|
||||
## Script Integration
|
||||
|
||||
```html
|
||||
<!-- Add this script in <head> -->
|
||||
<script src="https://unpkg.com/thirdweb/dist/scripts/bridge-widget.js"></script>
|
||||
|
||||
<!-- Add a container element where you want to render the UI -->
|
||||
<div id="bridge-widget-container"></div>
|
||||
|
||||
<!-- Initialize the widget at the end of <body> -->
|
||||
<script>
|
||||
const container = document.querySelector('#bridge-widget-container');
|
||||
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: "dark",
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
You can customize theme, currencies, token selections, and more. See the full list of options below
|
||||
|
||||
<Details summary="View full type">
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
clientId: string;
|
||||
theme?: "dark" | "light" | {
|
||||
type: "dark" | "light";
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success?: string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
};
|
||||
showThirdwebBranding?: boolean;
|
||||
currency?: SupportedFiatCurrency;
|
||||
swap?: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
buy: {
|
||||
amount: string;
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
purchaseData?: PurchaseData;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
### clientId
|
||||
Set your Project's client ID to identify your project. You create a project for free on [thirdweb dashboard](https://thirdweb.com/team)
|
||||
|
||||
|
||||
### swap
|
||||
The options to configure the "Swap" tab
|
||||
|
||||
```ts
|
||||
type Swap = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="prefill">
|
||||
|
||||
You can prefill Buy and/or Sell tokens for the swap tab. If `tokenAddress` is not provided, the native token will be used. Here are some examples:
|
||||
|
||||
|
||||
#### Set an ERC20 token as the buy token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set a native token as the sell token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base native token (ETH)
|
||||
sellToken: {
|
||||
chainId: 8453,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set amount and token to Buy by default
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set both buy and sell tokens by default
|
||||
|
||||
```ts
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
// Polygon native token (POL)
|
||||
sellToken: {
|
||||
chainId: 137,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="persistTokenSelections">
|
||||
|
||||
Whether to persist the token selections to localStorage so that if the user revisits the widget, the last used tokens are pre-selected.
|
||||
|
||||
The last used tokens do not override the tokens specified in the `prefill` option. By default, it is set to `true`.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onDisconnect">
|
||||
|
||||
Pass a callback to be called when the user disconnects the active wallet.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
### buy
|
||||
|
||||
The options to configure the "Buy" tab UI.
|
||||
|
||||
|
||||
```ts
|
||||
type Buy = {
|
||||
amount: string; // Required
|
||||
chainId: number; // Required
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="amount" tags={["Required"]}>
|
||||
|
||||
Set the default token amount shown in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="chainId" tags={["Required"]}>
|
||||
|
||||
Specify which chain should be selected in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="tokenAddress">
|
||||
|
||||
Specify which token should be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="buttonLabel">
|
||||
|
||||
Override the label for main action button
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="country">
|
||||
|
||||
The user's ISO 3166 alpha-2 country code. This is used to determine onramp provider support.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="presetOptions">
|
||||
|
||||
Preset fiat amounts to display in the UI. Defaults to [5, 10, 20].
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### theme
|
||||
|
||||
Set the theme for the widget. You can either set it to `"dark"`, `"light"` or overrides some or all the colors by passing an object. By default it set to `"dark"`.
|
||||
|
||||
When overriding the colors, you must set the `type` to either `"dark"` or `"light"` so that the widget knows which theme to apply the overrides to.
|
||||
|
||||
|
||||
```ts
|
||||
type Theme = "dark" | "light" | {
|
||||
type: "dark" | "light"; // required
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success? : string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: {
|
||||
type: "dark",
|
||||
colors: {
|
||||
modalBg: "black",
|
||||
primaryText: "white",
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### currency
|
||||
|
||||
The token amounts fiat values will be displayed in this currency. By default, it is set to `"USD"`.
|
||||
|
||||
|
||||
```ts
|
||||
type Currency = "USD" | "EUR" | "GBP" | "JPY" | "KRW" | "CNY" | "INR" | "NOK" | "SEK" | "CHF" | "AUD" | "CAD" | "NZD" | "MXN" | "BRL" | "CLP" | "CZK" | "DKK" | "HKD" | "HUF" | "IDR" | "ILS" | "ISK";
|
||||
```
|
||||
|
||||
|
||||
### showThirdwebBranding
|
||||
|
||||
Whether to show thirdweb branding at the bottom of the widget. By default, it is set to `true`.
|
||||
@@ -102,17 +102,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="react">
|
||||
The quickest way to setup Bridge in your React app is with the [`BuyWidget`](/references/typescript/v5/widgets/BuyWidget) component.
|
||||
|
||||
The quickest way to setup Bridge in your React app is with the [`SwapWidget`](/references/typescript/v5/widgets/SwapWidget) component.
|
||||
|
||||
### Live Playground
|
||||
|
||||
<ArticleIconCard
|
||||
title="Payment Widgets Playground"
|
||||
description="Try out the Payment Widgets in our live playground"
|
||||
title="Swap Widget Playground"
|
||||
description="Try out the Swap Widget in our live playground"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/connect/pay/transactions"
|
||||
href="https://playground.thirdweb.com/bridge/swap-widget"
|
||||
/>
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
Install the thirdweb SDK in your React project:
|
||||
@@ -127,7 +127,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Create a Client
|
||||
|
||||
First, create a client file (e.g., `thirdwebClient.ts`) for reuse throughout your app:
|
||||
First, create a client file for reuse throughout your app:
|
||||
|
||||
```typescript
|
||||
// thirdwebClient.ts
|
||||
@@ -140,7 +140,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Setup the Provider
|
||||
|
||||
Wrap your application with the ThirdwebProvider:
|
||||
Wrap your application with the `ThirdwebProvider`
|
||||
|
||||
```tsx
|
||||
// app.tsx / _app.tsx
|
||||
@@ -155,21 +155,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
}
|
||||
```
|
||||
|
||||
### Buy Widget
|
||||
### Add Swap Widget
|
||||
|
||||
Use the BuyWidget to let users purchase tokens:
|
||||
Use the SwapWidget to let users swap tokens
|
||||
|
||||
```tsx
|
||||
import { BuyWidget } from "thirdweb/react";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { SwapWidget } from "thirdweb/react";
|
||||
|
||||
function PaymentComponent() {
|
||||
function Example() {
|
||||
return (
|
||||
<BuyWidget
|
||||
<SwapWidget
|
||||
client={client}
|
||||
chain={base}
|
||||
amount="10" // 10 USDC
|
||||
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ export const sidebar: SideBar = {
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="container max-w-3xl py-10" data-noindex>
|
||||
<main className="container max-w-4xl py-10" data-noindex>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<div className="changelog-page">
|
||||
<Button asChild>
|
||||
<Button asChild variant="ghost">
|
||||
<Link
|
||||
className="-translate-x-1/4 !p-2 !text-muted-foreground hover:!text-foreground mb-4 bg-transparent"
|
||||
href="/changelog"
|
||||
@@ -64,7 +64,7 @@ export default async function Page(props: {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mb-8 border-t-2" />
|
||||
<div className="mb-8 border-t border-dashed" />
|
||||
|
||||
{ReactHtmlParser(data.html || "", {
|
||||
transform,
|
||||
|
||||
@@ -6,13 +6,11 @@ export function Author(props: { name: string; profileImage?: string | null }) {
|
||||
{props.profileImage && (
|
||||
<img
|
||||
alt=""
|
||||
className="size-8 rounded-[50%] border"
|
||||
className="size-6 rounded-[50%] border"
|
||||
src={props.profileImage}
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium text-muted-foreground text-sm">
|
||||
{props.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">{props.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TableOfContentsSideBar } from "../../../components/others/TableOfContents";
|
||||
|
||||
const onlyShowPostTitle = (h: HTMLHeadingElement) => {
|
||||
return h.classList.contains("changelog-title");
|
||||
};
|
||||
|
||||
export function ChangelogIndexTOC() {
|
||||
return <TableOfContentsSideBar filterHeading={onlyShowPostTitle} />;
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
import { formatDistance } from "date-fns";
|
||||
import { formatDate } from "date-fns";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function RenderDate(props: { iso: string; className?: string }) {
|
||||
return (
|
||||
<time
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
props.className,
|
||||
)}
|
||||
dateTime={props.iso}
|
||||
>
|
||||
{formatDistance(new Date(props.iso), new Date())} ago
|
||||
<time className={cn("text-sm", props.className)} dateTime={props.iso}>
|
||||
{formatDate(new Date(props.iso), "MMM d, yyyy")}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { createMetadata } from "@doc";
|
||||
import GithubSlugger from "github-slugger";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import ReactHtmlParser from "react-html-parser";
|
||||
import { Heading } from "@/components/Document";
|
||||
import { Author } from "./components/Author";
|
||||
import { ChangelogIndexTOC } from "./components/ChangeLogIndexTOC";
|
||||
import { RenderDate } from "./components/RenderData";
|
||||
import { fetchChangeLogs, fetchPost } from "./ghost";
|
||||
import { transform } from "./utils/transform";
|
||||
@@ -23,12 +20,15 @@ export const metadata = createMetadata({
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<main className="container" data-noindex>
|
||||
<div className="flex justify-between gap-16">
|
||||
<div className="w-full max-w-[850px] grow-0 ">
|
||||
<PageContent />
|
||||
<main data-noindex>
|
||||
<div className="border-b py-12 border-dashed">
|
||||
<div className="container">
|
||||
<h1 className="font-semibold text-4xl tracking-tight">Changelog</h1>
|
||||
</div>
|
||||
<ChangelogIndexTOC />
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<PageContent />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -39,53 +39,48 @@ async function PageContent() {
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
return (
|
||||
<div className="changelog-page py-6">
|
||||
<h1 className="mb-10 font-semibold text-3xl tracking-tighter">
|
||||
Changelog
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-col gap-10 xl:border-l-2 xl:pl-12">
|
||||
<div className="changelog-page overflow-hidden">
|
||||
<div className="flex flex-col gap-10 xl:border-l border-dashed xl:pl-16 max-w-4xl ml-auto pb-20">
|
||||
{posts.map((post) => {
|
||||
return (
|
||||
<div className="relative pb-10" key={post.id}>
|
||||
<div className="mb-2 flex items-center gap-5">
|
||||
<div className="relative pt-10" key={post.id}>
|
||||
<div className="-left-16 -translate-x-full absolute top-12 hidden xl:flex gap-5 ml-3 items-center">
|
||||
{post.published_at && (
|
||||
<RenderDate className="text-base" iso={post.published_at} />
|
||||
<div className="hidden xl:block text-sm text-muted-foreground">
|
||||
<RenderDate iso={post.published_at} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-5">
|
||||
{post.authors?.map((author) => {
|
||||
return (
|
||||
<Author
|
||||
key={author.id}
|
||||
name={author.name || ""}
|
||||
profileImage={author.profile_image}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="size-6 rounded-[50%] border bg-background flex items-center justify-center">
|
||||
<div className="size-2 bg-blue-500 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="-left-12 -translate-x-1/2 absolute top-12 hidden size-7 items-center justify-center rounded-[50%] bg-foreground md:size-10 xl:flex">
|
||||
<PlusIcon className="size-6 text-background" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Heading
|
||||
anchorClassName="[&>a]:hidden m-0 border-b pb-5 mb-5"
|
||||
anchorId={slugger.slug(post.title || "")}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="changelog-title"
|
||||
level={2}
|
||||
>
|
||||
<Link
|
||||
className="!text-foreground font-bold text-3xl tracking-tighter hover:underline md:text-4xl xl:text-5xl"
|
||||
href={`/changelog/${post.slug}`}
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
{post.published_at && (
|
||||
<div className="xl:hidden inline-flex rounded-full border px-3 py-2 mb-4 text-muted-foreground text-xs bg-card">
|
||||
<RenderDate iso={post.published_at} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RenderChangelogContent slug={post.slug} />
|
||||
<div className="space-y-3 mb-5">
|
||||
<Heading
|
||||
anchorClassName="[&>a]:hidden m-0"
|
||||
anchorId={slugger.slug(post.title || "")}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="changelog-title"
|
||||
level={2}
|
||||
>
|
||||
<Link
|
||||
className="!text-foreground font-semibold text-3xl md:text-4xl tracking-tight hover:underline !leading-tight"
|
||||
href={`/changelog/${post.slug}`}
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
<RenderChangelogContent slug={post.slug} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export const transform: Transform = (node, index: number) => {
|
||||
node.attribs.class = "text-center text-sm text-muted-foreground";
|
||||
return;
|
||||
}
|
||||
return <Paragraph>{getChildren()}</Paragraph>;
|
||||
return <Paragraph className="leading-7">{getChildren()}</Paragraph>;
|
||||
}
|
||||
|
||||
if (node.name === "ul") {
|
||||
@@ -77,7 +77,7 @@ export const transform: Transform = (node, index: number) => {
|
||||
}
|
||||
node.attribs.class = "rounded-lg";
|
||||
return (
|
||||
<div className="my-5 flex justify-center rounded-lg border p-4">
|
||||
<div className="my-6 flex justify-center rounded-lg border border-dashed p-2 bg-card/50">
|
||||
{convertNodeToElement(node, index, transform)}
|
||||
</div>
|
||||
);
|
||||
@@ -109,7 +109,13 @@ export const transform: Transform = (node, index: number) => {
|
||||
// }
|
||||
}
|
||||
if (code) {
|
||||
return <CodeBlock code={code} lang={lang} />;
|
||||
return (
|
||||
<CodeBlock
|
||||
code={code}
|
||||
lang={lang}
|
||||
className="bg-card/50 border-dashed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
html {
|
||||
font-feature-settings: "liga" 1;
|
||||
}
|
||||
|
||||
code span {
|
||||
color: var(--code-light-color);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@workspace/ui/global.css";
|
||||
import "./globals.css";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { createMetadata } from "@/components/Document";
|
||||
@@ -9,18 +10,6 @@ import { EnableSmoothScroll } from "../components/others/SmoothScroll";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Header } from "./Header";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
const monoFont = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: "thirdweb developer portal",
|
||||
title: "thirdweb docs",
|
||||
@@ -34,7 +23,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(sansFont.variable, monoFont.variable, "font-sans")}
|
||||
className={cn(GeistMono.variable, GeistSans.variable, "font-sans")}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ThemeProvider
|
||||
|
||||
@@ -50,6 +50,7 @@ Here's an example with a Next.js middleware:
|
||||
```typescript
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { arbitrumSepolia } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
const thirdwebX402Facilitator = facilitator({
|
||||
@@ -67,7 +68,7 @@ export async function middleware(request: NextRequest) {
|
||||
method,
|
||||
paymentData,
|
||||
payTo: "0xYourWalletAddress",
|
||||
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
|
||||
network: arbitrumSepolia, // or any other chain
|
||||
price: "$0.01", // can also be a ERC20 token amount
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import GithubSlugger from "github-slugger";
|
||||
import { FileTextIcon, FolderOpenIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { TransformedDoc } from "typedoc-better-json";
|
||||
import {
|
||||
ArticleIconCard,
|
||||
createMetadata,
|
||||
Details,
|
||||
Heading,
|
||||
} from "@/components/Document";
|
||||
import { createMetadata, Details, Heading } from "@/components/Document";
|
||||
import { Breadcrumb } from "@/components/Document/Breadcrumb";
|
||||
import { DocLayout } from "@/components/Layouts/DocLayout";
|
||||
import type { LinkGroup, LinkMeta } from "@/components/others/Sidebar";
|
||||
import { sluggerContext } from "@/contexts/slugger";
|
||||
import type { MetadataImageIcon } from "../../../../components/Document/metadata";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
import { RootTDoc } from "./Root";
|
||||
import { getSidebarLinkGroups } from "./utils/getSidebarLinkgroups";
|
||||
import { fetchAllSlugs, getSlugToDocMap } from "./utils/slugs";
|
||||
@@ -330,10 +327,44 @@ function GroupOfLinks(props: { linkGroup: LinkGroup; level: number }) {
|
||||
accordionItemClassName="m-0"
|
||||
accordionTriggerClassName="rounded-lg"
|
||||
anchorId={slugger.slug(props.linkGroup.name)}
|
||||
headingClassName="py-0.5 text-lg"
|
||||
headingClassName="py-0.5 text-lg font-medium"
|
||||
summary={props.linkGroup.name}
|
||||
>
|
||||
<RenderLinkGroup level={props.level + 1} linkGroup={props.linkGroup} />
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
|
||||
function ArticleIconCard(props: {
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
const isExternal = props.href.startsWith("http");
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-4 rounded-xl border bg-card p-4 transition-colors hover:border-active-border",
|
||||
props.className,
|
||||
)}
|
||||
data-noindex
|
||||
href={props.href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
>
|
||||
{props.icon && (
|
||||
<div className="shrink-0">
|
||||
<div className="rounded-full p-2.5 bg-background border">
|
||||
<props.icon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="font-semibold text-base text-foreground leading-none">
|
||||
{props.title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
- 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).
|
||||
|
||||
<OpenApiEndpoint path="/v1/transactions" method="POST" />
|
||||
<OpenApiEndpoint path="/v1/transactions" method="POST" />
|
||||
|
||||
</TabsContent>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="typescript">
|
||||
### Installation
|
||||
@@ -425,47 +425,47 @@ Send, monitor, and manage transactions. Send transactions from user or server wa
|
||||
2. Enter your Client ID from the thirdweb dashboard
|
||||
3. Allowlist your game's Bundle ID on the thirdweb dashboard for security
|
||||
|
||||
### Send a Transaction
|
||||
### Send a Transaction
|
||||
|
||||
Create a new script to manage transactions:
|
||||
Create a new script to manage transactions:
|
||||
|
||||
```csharp
|
||||
using Thirdweb;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
```csharp
|
||||
using Thirdweb;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class TransactionManager : MonoBehaviour
|
||||
{
|
||||
private ThirdwebSDK sdk;
|
||||
public Text walletAddressText;
|
||||
public Button sendButton;
|
||||
public class TransactionManager : MonoBehaviour
|
||||
{
|
||||
private ThirdwebSDK sdk;
|
||||
public Text walletAddressText;
|
||||
public Button sendButton;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Client ID is set in Project Settings > Thirdweb
|
||||
sdk = new ThirdwebSDK("ethereum"); // Or any supported chain
|
||||
sendButton.onClick.AddListener(SendTransaction);
|
||||
}
|
||||
void Start()
|
||||
{
|
||||
// Client ID is set in Project Settings > Thirdweb
|
||||
sdk = new ThirdwebSDK("ethereum"); // Or any supported chain
|
||||
sendButton.onClick.AddListener(SendTransaction);
|
||||
}
|
||||
|
||||
public async void SendTransaction()
|
||||
{
|
||||
try {
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "contract-address",
|
||||
chainId: 1,
|
||||
abi: "optional-abi"
|
||||
);
|
||||
public async void SendTransaction()
|
||||
{
|
||||
try {
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "contract-address",
|
||||
chainId: 1,
|
||||
abi: "optional-abi"
|
||||
);
|
||||
|
||||
// Send a transaction
|
||||
var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount);
|
||||
Debug.Log($"Transaction receipt: {receipt}");
|
||||
}
|
||||
catch (System.Exception e) {
|
||||
Debug.LogError("Error sending transaction: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
// Send a transaction
|
||||
var receipt = await contract.Write(wallet, contract, "transfer", weiValue, toAddress, amount);
|
||||
Debug.Log($"Transaction receipt: {receipt}");
|
||||
}
|
||||
catch (System.Exception e) {
|
||||
Debug.LogError("Error sending transaction: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ Example sponsored contract write request:
|
||||
|
||||
</Tabs>
|
||||
|
||||
<div className='h-6' />
|
||||
|
||||
That's it! All transactions executed by the user will be sponsored via the thirdweb infrastructure.
|
||||
|
||||
</TabsContent>
|
||||
|
||||
@@ -19,7 +19,7 @@ Vault is an open-source non-custodial key management service, secured with TEE a
|
||||
## Features
|
||||
|
||||
<div
|
||||
className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-2 "
|
||||
className="my-4 space-y-4"
|
||||
>
|
||||
<FeatureCard
|
||||
title="Non-Custodial Wallets"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { MessageCircleIcon, RefreshCcwIcon, XIcon } from "lucide-react";
|
||||
import { BrainIcon, RefreshCcwIcon, XIcon } from "lucide-react";
|
||||
import { lazy, Suspense, useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
@@ -20,14 +20,14 @@ export function ChatButton() {
|
||||
<>
|
||||
{/* Inline Button (not floating) */}
|
||||
<Button
|
||||
className="gap-2 rounded-full shadow-lg"
|
||||
className="gap-2 rounded-full bg-background"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
setHasBeenOpened(true);
|
||||
}}
|
||||
variant="default"
|
||||
variant="outline"
|
||||
>
|
||||
<MessageCircleIcon className="size-4" />
|
||||
<BrainIcon className="size-4 text-muted-foreground" />
|
||||
Ask AI
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@ export function ApiEndpoint(props: { metadata: ApiEndpointMeta }) {
|
||||
<div>
|
||||
<Heading
|
||||
anchorId={`request#${props.metadata.method.toLowerCase()}${props.metadata.path}`}
|
||||
className="text-lg lg:text-lg"
|
||||
level={3}
|
||||
>
|
||||
Request
|
||||
|
||||
@@ -73,7 +73,7 @@ export function RequestExample(props: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex-col lg:flex-row flex items-start lg:items-center justify-between border-b px-4 py-2 gap-3 lg:gap-0">
|
||||
<div className="flex-col lg:flex-row flex items-start lg:items-center justify-between border-b px-4 py-3 gap-3 lg:gap-0">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Badge className="bg-inverted text-inverted-foreground">
|
||||
{props.method}
|
||||
@@ -109,10 +109,10 @@ export function RequestExample(props: {
|
||||
}}
|
||||
value={selectedFormat}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectTrigger className="w-[130px] rounded-full bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background">
|
||||
<SelectContent className="bg-background rounded-xl">
|
||||
<SelectItem value="fetch">Fetch</SelectItem>
|
||||
<SelectItem value="curl">Curl</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -144,10 +144,10 @@ export function RequestExample(props: {
|
||||
}}
|
||||
value={selectedExampleType}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-[130px] rounded-full bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background">
|
||||
<SelectContent className="bg-background rounded-xl">
|
||||
{exampleTypes.map((exampleType) => (
|
||||
<SelectItem key={exampleType} value={exampleType!}>
|
||||
{exampleType}
|
||||
@@ -169,10 +169,10 @@ export function RequestExample(props: {
|
||||
}}
|
||||
value={selectedExample?.label}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectTrigger className="w-[130px] rounded-full bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background">
|
||||
<SelectContent className="bg-background rounded-xl">
|
||||
{props.codeExamples.map((example) => (
|
||||
<SelectItem key={example.label} value={example.label}>
|
||||
{example.label}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CodeClient } from "../code/code.client";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
type AuthMethod =
|
||||
| "email"
|
||||
@@ -849,16 +850,15 @@ function AuthMethodsTabsContent() {
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{authMethods.map((method) => (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
key={method.id}
|
||||
onClick={() => setSelectedAuth(method.id)}
|
||||
className={cn(
|
||||
"overflow-hidden text-ellipsis px-3 py-1.5 transition-colors duration-300 hover:text-foreground text-sm rounded-lg hover:bg-violet-800/15",
|
||||
selectedAuth === method.id
|
||||
? "font-medium text-foreground bg-violet-800/25 border border-violet-800"
|
||||
: "",
|
||||
"flex flex-row justify-start items-start gap-3",
|
||||
"overflow-hidden text-ellipsis text-sm rounded-lg hover:bg-accent justify-start gap-3 h-auto py-3 rounded-xl",
|
||||
selectedAuth === method.id &&
|
||||
"font-medium text-foreground bg-accent",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
@@ -866,8 +866,8 @@ function AuthMethodsTabsContent() {
|
||||
className="size-5 shrink-0"
|
||||
src={getSocialIcon(method.id)}
|
||||
/>
|
||||
<div className="font-medium">{method.label}</div>
|
||||
</button>
|
||||
<div>{method.label}</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ArticleIconCard(props: {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-4 rounded-lg border bg-card p-4 transition-colors hover:border-active-border",
|
||||
"flex items-center gap-4 rounded-xl border bg-card px-4 py-6 transition-colors hover:border-active-border",
|
||||
props.className,
|
||||
)}
|
||||
data-noindex
|
||||
@@ -48,17 +48,21 @@ export function ArticleIconCard(props: {
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
>
|
||||
{props.icon && (
|
||||
<props.icon className="size-6 shrink-0 text-muted-foreground" />
|
||||
<div className="shrink-0">
|
||||
<div className="rounded-full p-2.5 bg-background border">
|
||||
<props.icon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{props.image && (
|
||||
<Image alt="" className="size-6 shrink-0" src={props.image} />
|
||||
)}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="font-semibold text-base text-foreground">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="font-semibold text-base text-foreground leading-none">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.description && (
|
||||
<p className="text-muted-foreground">{props.description}</p>
|
||||
<p className="text-muted-foreground text-sm">{props.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -67,14 +67,14 @@ export async function CodeBlock(props: {
|
||||
<div className={cn("group/code relative mb-5", props.containerClassName)}>
|
||||
<code
|
||||
className={cn(
|
||||
"relative block whitespace-pre rounded-lg border bg-card font-mono text-sm leading-relaxed",
|
||||
"relative block whitespace-pre rounded-lg border bg-card dark:bg-background font-mono text-sm leading-relaxed",
|
||||
props.className,
|
||||
)}
|
||||
lang={lang}
|
||||
>
|
||||
<ScrollShadow
|
||||
className=""
|
||||
scrollableClassName={cn("p-4", props.scrollContainerClassName)}
|
||||
shadowColor="hsl(var(--card))"
|
||||
>
|
||||
<RenderCode code={code} lang={lang} tokenLinks={tokenLinks} />
|
||||
</ScrollShadow>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Details(props: {
|
||||
<div className="flex gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"break-all font-bold text-foreground text-sm tracking-tight",
|
||||
"break-all text-foreground text-lg",
|
||||
"flex w-full gap-3 text-left font-semibold text-foreground group-hover:underline",
|
||||
props.headingClassName,
|
||||
)}
|
||||
@@ -58,7 +58,9 @@ export function Details(props: {
|
||||
props.accordionTriggerClassName,
|
||||
)}
|
||||
>
|
||||
<div className="pl-4 [&>:first-child]:mt-0">{props.children}</div>
|
||||
<div className="pl-4 [&>:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||
{props.children}
|
||||
</div>
|
||||
</CustomAccordion>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function DocImage(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"my-4 flex justify-center rounded-lg border p-4",
|
||||
"my-4 flex justify-center rounded-lg border border-dashed p-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function DocLink(props: {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"underline decoration-violet-800 decoration-dotted underline-offset-[5px] transition-colors hover:decoration-violet-800 hover:decoration-solid hover:text-foreground",
|
||||
"text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400 decoration-dotted transition-colors ",
|
||||
props.className,
|
||||
)}
|
||||
href={props.href}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "../ui/button";
|
||||
import { GithubIcon } from "./GithubButtonLink";
|
||||
|
||||
const prefix =
|
||||
@@ -6,15 +7,11 @@ const prefix =
|
||||
|
||||
export function EditPage(props: { path: string }) {
|
||||
return (
|
||||
<Link
|
||||
className="inline-flex items-center rounded-lg border text-sm duration-200 hover:border-active-border"
|
||||
href={prefix + props.path}
|
||||
target="_blank"
|
||||
>
|
||||
<div className="p-2.5">
|
||||
<GithubIcon className="size-5" />
|
||||
</div>
|
||||
<div className="border-l-2 p-2.5 font-semibold">Edit this page</div>
|
||||
</Link>
|
||||
<Button variant="outline" className="gap-2 rounded-xl bg-card" asChild>
|
||||
<Link href={prefix + props.path} target="_blank">
|
||||
<GithubIcon className="size-4 text-muted-foreground" />
|
||||
Edit this page
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type FeatureCardItem = {
|
||||
export function FeatureCard(props: FeatureCardItem) {
|
||||
const { title, description, iconUrl } = props;
|
||||
return (
|
||||
<div className="bg-card rounded-lg p-4 border">
|
||||
<div className="bg-card rounded-xl p-4 border">
|
||||
<div className="flex mb-3">
|
||||
<div className="p-2 rounded-full border bg-background [&_svg]:size-4 [&_svg]:text-muted-foreground">
|
||||
{typeof iconUrl === "string" ? (
|
||||
|
||||
@@ -4,7 +4,7 @@ export function Grid(props: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"my-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3",
|
||||
"my-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function Heading(props: {
|
||||
return (
|
||||
<h1
|
||||
className={cn(
|
||||
"mb-5 break-words font-semibold text-3xl text-foreground tracking-tight",
|
||||
"mb-5 break-words font-bold text-3xl text-foreground tracking-tight",
|
||||
props.className,
|
||||
)}
|
||||
data-noindex={props.noIndex}
|
||||
@@ -33,7 +33,7 @@ export function Heading(props: {
|
||||
>
|
||||
<h2
|
||||
className={cn(
|
||||
"break-words font-medium text-xl text-foreground tracking-tight",
|
||||
"break-words font-semibold text-2xl text-foreground tracking-tight",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -46,13 +46,13 @@ export function Heading(props: {
|
||||
case 3: {
|
||||
return (
|
||||
<Anchor
|
||||
className={cn("mt-10 mb-3", props.anchorClassName)}
|
||||
className={cn("mt-10 mb-2", props.anchorClassName)}
|
||||
data-noindex={props.noIndex}
|
||||
id={props.anchorId}
|
||||
>
|
||||
<h3
|
||||
className={cn(
|
||||
"break-words font-medium text-foreground text-xl",
|
||||
"break-words font-semibold text-foreground text-xl",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -71,7 +71,7 @@ export function Heading(props: {
|
||||
>
|
||||
<h4
|
||||
className={cn(
|
||||
"break-words font-medium text-foreground text-lg",
|
||||
"break-words font-semibold text-foreground text-lg",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ export function InlineCode(props: { code: string; className?: string }) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
"max-h-20 rounded-md border bg-muted px-1.5 py-0.5 text-[0.875em]",
|
||||
"max-h-20 rounded-lg bg-muted/70 border px-1.5 py-[3px] text-[0.875em]",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function NextPageButton(props: { href: string; name: string }) {
|
||||
return (
|
||||
<Link
|
||||
className="inline-flex items-center rounded-lg border text-sm duration-200 hover:border-active-border"
|
||||
href={props.href}
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<div className="border-r-2 p-2.5 font-semibold">Next: {props.name}</div>
|
||||
<div className="p-2.5">
|
||||
<ArrowRightIcon className="size-5" />
|
||||
</div>
|
||||
<span> {props.name}</span>
|
||||
<ChevronRightIcon className="size-5" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,18 +16,17 @@ export function PageFooter(props: {
|
||||
sidebarLinks?: SidebarLink[];
|
||||
}) {
|
||||
return (
|
||||
<footer className="flex flex-col gap-7 pb-20" data-noindex>
|
||||
<div className="flex flex-col justify-between gap-7">
|
||||
<div className="flex gap-4 justify-between items-center">
|
||||
{props.editPageButton && <AutoEditPageButton />}
|
||||
{props.sidebarLinks && (
|
||||
<AutoNextPageButton sidebarLinks={props.sidebarLinks} />
|
||||
)}
|
||||
<footer className="flex flex-col" data-noindex>
|
||||
{props.sidebarLinks && (
|
||||
<div className="flex gap-4 justify-end items-center py-6">
|
||||
<AutoNextPageButton sidebarLinks={props.sidebarLinks} />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t py-6 border-dashed flex flex-col items-start gap-4 md:flex-row md:justify-between md:items-center">
|
||||
{props.editPageButton && <AutoEditPageButton />}
|
||||
<Feedback />
|
||||
</div>
|
||||
<div className="h-1 border-t" />
|
||||
<div className="flex flex-col justify-between gap-7 md:flex-row">
|
||||
<div className="flex flex-col justify-between gap-7 md:flex-row border-t py-6 border-dashed">
|
||||
<Links />
|
||||
<Subscribe />
|
||||
</div>
|
||||
@@ -37,18 +36,18 @@ export function PageFooter(props: {
|
||||
|
||||
function Links() {
|
||||
return (
|
||||
<ul className="flex flex-col gap-3">
|
||||
<ul className="flex flex-col gap-2">
|
||||
<FooterLinkItem
|
||||
href="https://www.youtube.com/@thirdweb_"
|
||||
icon={VideoIcon}
|
||||
label="video Tutorials"
|
||||
label="Video tutorials"
|
||||
prefix="Watch our"
|
||||
/>
|
||||
|
||||
<FooterLinkItem
|
||||
href="/changelog"
|
||||
icon={ScrollTextIcon}
|
||||
label="changelog"
|
||||
label="Changelog"
|
||||
prefix="View our"
|
||||
/>
|
||||
|
||||
@@ -71,11 +70,13 @@ function FooterLinkItem(props: {
|
||||
return (
|
||||
<li className="flex items-center gap-2 text-muted-foreground">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<props.icon className="size-5" />
|
||||
<span>{props.prefix}</span>
|
||||
<props.icon className="size-4" />
|
||||
<span className="text-sm">{props.prefix}</span>
|
||||
</div>
|
||||
|
||||
<DocLink href={props.href}>{props.label}</DocLink>
|
||||
<DocLink href={props.href} className="text-sm">
|
||||
{props.label}
|
||||
</DocLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,16 @@ export function SDKCard(props: {
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
className="flex items-center gap-2 rounded-lg border bg-card p-2 px-4 transition-colors hover:border-active-border text-foreground/80 hover:text-foreground shadow-sm"
|
||||
className="flex items-center gap-3 rounded-xl border bg-card px-4 py-3.5 transition-colors hover:border-active-border text-foreground/80 hover:text-foreground shadow-sm"
|
||||
href={props.href}
|
||||
target={props.isExternal ? "_blank" : undefined}
|
||||
>
|
||||
{props.icon && (
|
||||
<props.icon className={cn("size-4 shrink-0", props.iconClassName)} />
|
||||
<div className="rounded-full p-1.5 bg-background shrink-0 border">
|
||||
<props.icon className={cn("size-4", props.iconClassName)} />
|
||||
</div>
|
||||
)}
|
||||
<h3 className="font-semibold text-sm tracking-tight">{props.title}</h3>
|
||||
<h3 className="text-foreground text-sm font-medium">{props.title}</h3>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.steps [data-step]::before {
|
||||
content: counter(step);
|
||||
@apply bg-inverted rounded-[50%] text-inverted-foreground text-base w-8 h-8 inline-flex items-center justify-center absolute left-0 font-semibold;
|
||||
@apply bg-card border border-border rounded-full text-foreground text-sm w-8 h-8 inline-flex items-center justify-center absolute left-0;
|
||||
}
|
||||
|
||||
.steps [data-step] {
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from "./Steps.module.css";
|
||||
export function Steps(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={cn("pl-4", styles.steps, "relative my-5")}>
|
||||
<ul className="border-l py-4 pl-8 [&_li:first-child_[data-step]_div:first-child]:mt-0">
|
||||
<ul className="border-l border-dashed py-4 pl-8 [&_li:first-child_[data-step]_div:first-child]:mt-0">
|
||||
{props.children}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function DocLayout(props: DocLayoutProps) {
|
||||
"container relative flex flex-col gap-12 xl:grid",
|
||||
props.sideBar.links.length > 0
|
||||
? props.showTableOfContents !== false
|
||||
? "xl:grid-cols-[220px_720px_1fr]"
|
||||
? "xl:grid-cols-[220px_820px_1fr]"
|
||||
: "xl:grid-cols-[220px_1160px]"
|
||||
: props.showTableOfContents !== false
|
||||
? "xl:grid-cols-[960px_1fr]"
|
||||
@@ -49,7 +49,7 @@ export function DocLayout(props: DocLayoutProps) {
|
||||
<DocSidebar {...props.sideBar} header={props.sidebarHeader} />
|
||||
</aside>
|
||||
)}
|
||||
<div className="sticky top-sticky-top-height z-stickyMobileSidebar border-b bg-background py-4 xl:hidden">
|
||||
<div className="sticky top-sticky-top-height z-stickyMobileSidebar border-b bg-card/50 backdrop-blur-xl p-4 xl:hidden -mx-4">
|
||||
<DocSidebarMobile {...props.sideBar} header={props.sidebarHeader} />
|
||||
</div>
|
||||
<main
|
||||
@@ -58,7 +58,7 @@ export function DocLayout(props: DocLayoutProps) {
|
||||
data-noindex={props.noIndex}
|
||||
>
|
||||
<div className="grow xl:mt-6">
|
||||
<h5 className="mb-2 text-sm font-semibold text-violet-500">
|
||||
<h5 className="mb-2 text-sm text-muted-foreground">
|
||||
{props.sideBar.name}
|
||||
</h5>
|
||||
{props.children}
|
||||
|
||||
@@ -146,11 +146,11 @@ function SearchModalContent(props: { closeModal: () => void }) {
|
||||
<div className="min-h-[200px]">
|
||||
{/* tags */}
|
||||
{enabledTags && enabledTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 border-b p-4">
|
||||
<div className="flex flex-wrap gap-2 border-b p-4 bg-card/50">
|
||||
{enabledTags.map((tag) => (
|
||||
<Button
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-1 text-sm ",
|
||||
"rounded-lg border px-2.5 py-1.5 text-xs h-auto",
|
||||
selectedTags[tag]
|
||||
? "!bg-muted !text-foreground border-foreground"
|
||||
: "!bg-card !text-muted-foreground",
|
||||
@@ -199,7 +199,7 @@ function SearchModalContent(props: { closeModal: () => void }) {
|
||||
{/* links */}
|
||||
{data && data.length > 0 && (
|
||||
<div
|
||||
className="styled-scrollbar flex max-h-[50vh] min-h-[200px] flex-col gap-2 overflow-y-auto p-4"
|
||||
className="styled-scrollbar flex max-h-[50vh] min-h-[200px] flex-col gap-1 overflow-y-auto p-2"
|
||||
ref={scrollableElement}
|
||||
>
|
||||
{data.map((result) => {
|
||||
@@ -221,7 +221,7 @@ function SearchModalContent(props: { closeModal: () => void }) {
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2" key={result.pageHref}>
|
||||
<div className="flex flex-col gap-1" key={result.pageHref}>
|
||||
<SearchResultItem
|
||||
href={result.pageHref}
|
||||
onClick={handleLinkClick}
|
||||
@@ -231,23 +231,25 @@ function SearchModalContent(props: { closeModal: () => void }) {
|
||||
/>
|
||||
|
||||
{sections && sections.length > 0 && (
|
||||
<div className="flex flex-col gap-2 border-l pl-3">
|
||||
{sections.map((sectionData) => {
|
||||
return (
|
||||
<SearchResultItem
|
||||
content={
|
||||
sectionData.content.length < 100
|
||||
? sectionData.content
|
||||
: `${sectionData.content.slice(0, 100)} ...`
|
||||
}
|
||||
href={result.pageHref + sectionData.href}
|
||||
key={sectionData.href}
|
||||
onClick={handleLinkClick}
|
||||
title={sectionData.title}
|
||||
type="section"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="pl-4">
|
||||
<div className="flex flex-col gap-1 border-l pl-3 border-dashed">
|
||||
{sections.map((sectionData) => {
|
||||
return (
|
||||
<SearchResultItem
|
||||
content={
|
||||
sectionData.content.length < 100
|
||||
? sectionData.content
|
||||
: `${sectionData.content.slice(0, 100)} ...`
|
||||
}
|
||||
href={result.pageHref + sectionData.href}
|
||||
key={sectionData.href}
|
||||
onClick={handleLinkClick}
|
||||
title={sectionData.title}
|
||||
type="section"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -274,7 +276,7 @@ function SearchModalContent(props: { closeModal: () => void }) {
|
||||
|
||||
function NoSearchLinks(props: { onClick?: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<div className="flex flex-col gap-1 p-2">
|
||||
{suggestedLinks.map((link) => {
|
||||
return (
|
||||
<SearchResultItem
|
||||
@@ -355,10 +357,10 @@ export function DocSearch(props: { variant: "icon" | "search" }) {
|
||||
{forDesktop && (
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex w-64 justify-between gap-6 px-3 text-muted-foreground bg-card"
|
||||
className="flex w-64 justify-between gap-6 px-4 text-muted-foreground bg-background rounded-xl"
|
||||
variant="outline"
|
||||
>
|
||||
<span className="text-xs">Search Docs</span>
|
||||
<span className="text-sm font-normal">Search Docs</span>
|
||||
<div className="flex items-center gap-1 rounded-sm p-1 text-muted-foreground text-xs">
|
||||
<CommandIcon className="size-3" />K
|
||||
</div>
|
||||
@@ -374,7 +376,7 @@ export function DocSearch(props: { variant: "icon" | "search" }) {
|
||||
</DialogTrigger>
|
||||
)}
|
||||
|
||||
<DialogContent className="bg-background sm:max-w-[550px]">
|
||||
<DialogContent className="bg-background sm:max-w-[550px] rounded-xl">
|
||||
<SearchModalContent
|
||||
closeModal={() => {
|
||||
setOpen(false);
|
||||
@@ -449,13 +451,13 @@ function SearchResultItem(props: {
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
className="flex gap-3 rounded-sm bg-muted/50 px-4 py-3 text-muted-foreground transition-colors hover:bg-accent"
|
||||
className="flex gap-3 rounded-lg px-3 py-2.5 text-muted-foreground transition-colors hover:bg-accent/50"
|
||||
href={props.href}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{props.title && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 break-all text-base text-foreground">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 break-all text-sm text-foreground">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
@@ -465,7 +467,7 @@ function SearchResultItem(props: {
|
||||
)}
|
||||
>
|
||||
{props.type === "page" && (
|
||||
<FileTextIcon className="size-5 text-muted-foreground" />
|
||||
<FileTextIcon className="size-4 text-muted-foreground/70" />
|
||||
)}
|
||||
|
||||
{props.title}
|
||||
@@ -477,7 +479,7 @@ function SearchResultItem(props: {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-lg border bg-muted px-1.5 py-1 text-muted-foreground text-xs",
|
||||
"shrink-0 rounded-lg border border-border/50 bg-muted/50 px-1.5 py-1 text-muted-foreground text-xs",
|
||||
)}
|
||||
key={tag}
|
||||
>
|
||||
@@ -489,7 +491,7 @@ function SearchResultItem(props: {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{props.content && <div className="text-sm">{props.content}</div>}
|
||||
{props.content && <div className="text-xs">{props.content}</div>}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,15 @@ import { BadgeCheckIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { reportFeedback } from "@/analytics/report";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
export function Feedback() {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
@@ -12,12 +20,11 @@ export function Feedback() {
|
||||
|
||||
if (!isSubmitted) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 md:h-16 md:flex-row md:items-center md:gap-6">
|
||||
<p>Was this page helpful?</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="md:h-16 flex items-center gap-4">
|
||||
<p className="text-sm text-foreground">Was this page helpful?</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="gap-2 bg-card"
|
||||
className="gap-2 bg-card rounded-lg"
|
||||
onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
reportFeedback({ helpful: true });
|
||||
@@ -30,22 +37,23 @@ export function Feedback() {
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2 bg-card" variant="outline">
|
||||
<Button className="gap-2 bg-card rounded-lg" variant="outline">
|
||||
No
|
||||
<ThumbsDownIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="p-5">
|
||||
<h3 className="mb-3 font-semibold text-foreground text-lg">
|
||||
Apologies for any confusion.
|
||||
</h3>
|
||||
<p className="mb-5 font-medium text-muted-foreground">
|
||||
Please provide details about the issue you encountered to help
|
||||
us improve our documentation.
|
||||
</p>
|
||||
<textarea
|
||||
className="mb-2 h-32 w-full rounded-sm border bg-card p-2 font-medium text-foreground outline-none placeholder:font-semibold"
|
||||
<DialogHeader className="mb-5">
|
||||
<DialogTitle>Apologies for any confusion.</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please provide details about the issue you encountered to help
|
||||
us improve our documentation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Textarea
|
||||
className="mb-2 h-32 w-full bg-card"
|
||||
onChange={(e) => {
|
||||
setFeedback(e.target.value);
|
||||
}}
|
||||
@@ -71,8 +79,8 @@ export function Feedback() {
|
||||
return (
|
||||
<div className="fade-in-0 animate-in duration-500">
|
||||
<div className="flex items-center gap-2 text-foreground md:h-16">
|
||||
<BadgeCheckIcon />
|
||||
<p className="font-semibold text-foreground">
|
||||
<BadgeCheckIcon className="size-4" />
|
||||
<p className="font-semibold text-foreground text-sm">
|
||||
Thank you for your feedback!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -18,19 +18,19 @@ export function PlatformSelector(props: { selected: Platform }) {
|
||||
const platform = connectLinks.find((p) => p.name === props.selected)!;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="inline-flex cursor-pointer gap-2 font-semibold text-base text-foreground">
|
||||
<platform.icon className="size-5" />
|
||||
{platform.name}
|
||||
<DropdownMenuTrigger>
|
||||
<div className="flex gap-2 font-medium text-sm text-foreground hover:bg-accent justify-between items-center py-1.5 px-2.5 rounded-lg">
|
||||
<div className="flex gap-2 items-center">
|
||||
<platform.icon className="size-4 text-foreground" />
|
||||
{platform.name}
|
||||
</div>
|
||||
<ChevronDownIcon className="w-4 text-muted-foreground opacity-70" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 bg-card p-3"
|
||||
style={{
|
||||
minWidth: "150px",
|
||||
}}
|
||||
className="flex flex-col gap-1 bg-card p-3 min-w-[200px] rounded-xl"
|
||||
sideOffset={6}
|
||||
>
|
||||
{connectLinks.map((platform) => {
|
||||
return (
|
||||
@@ -44,9 +44,9 @@ export function PlatformSelector(props: { selected: Platform }) {
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<platform.icon className="size-5 text-foreground" />
|
||||
<platform.icon className="size-4 text-foreground" />
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
className="before:absolute before:inset-0 text-sm"
|
||||
href={platform.href}
|
||||
>
|
||||
{platform.name}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DocSidebar(props: ReferenceSideBarProps) {
|
||||
<div className="flex h-full flex-col pb-10 pt-6 text-muted-foreground text-sm">
|
||||
{/* Side bar Name */}
|
||||
{props.header}
|
||||
<ul className="styled-scrollbar transform-gpu">
|
||||
<ul className="styled-scrollbar transform-gpu space-y-1">
|
||||
{props.links.map((link, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
|
||||
<li key={i}>
|
||||
@@ -74,7 +74,7 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
if ("separator" in props.link) {
|
||||
return <hr className="my-2 border-t" />;
|
||||
return <hr className="my-2 border-t border-dashed" />;
|
||||
}
|
||||
|
||||
const isActive = props.link.href
|
||||
@@ -105,10 +105,8 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
|
||||
return (
|
||||
<Link
|
||||
className={clsx(
|
||||
"overflow-hidden text-ellipsis px-3 py-1.5 transition-colors duration-300 hover:text-foreground hover:bg-violet-800/15 border border-transparent hover:border-violet-800/50 text-sm rounded-lg",
|
||||
isActive
|
||||
? "font-medium text-foreground bg-violet-800/25 border border-violet-800"
|
||||
: "",
|
||||
"overflow-hidden text-ellipsis px-3 py-1.5 transition-colors duration-300 hover:text-foreground hover:bg-accent text-sm rounded-lg",
|
||||
isActive ? "text-foreground !bg-accent" : "",
|
||||
"flex flex-row items-center gap-2",
|
||||
)}
|
||||
href={link.href}
|
||||
@@ -126,10 +124,8 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
|
||||
return (
|
||||
<Link
|
||||
className={clsx(
|
||||
"block overflow-hidden text-ellipsis px-3 py-1.5 transition-colors duration-300 hover:text-foreground hover:bg-violet-800/15 border border-transparent hover:border-violet-800/50 text-sm rounded-lg",
|
||||
isActive
|
||||
? "font-medium text-foreground bg-violet-800/25 border border-violet-800"
|
||||
: "",
|
||||
"block overflow-hidden text-ellipsis px-3 py-1.5 transition-colors duration-300 hover:text-foreground hover:bg-accent text-sm rounded-lg",
|
||||
isActive ? "text-foreground !bg-accent" : "",
|
||||
)}
|
||||
href={link.href}
|
||||
onClick={props.onLinkClick}
|
||||
@@ -155,8 +151,8 @@ function DocSidebarNonCollapsible(props: {
|
||||
{href ? (
|
||||
<Link
|
||||
className={cn(
|
||||
"block px-3 py-1.5 hover:bg-violet-800/20 w-full rounded-lg hover:border-violet-800/50 border border-transparent",
|
||||
isCategoryActive && "text-foreground",
|
||||
"block px-3 py-1.5 hover:bg-accent w-full rounded-lg",
|
||||
isCategoryActive && "text-foreground !bg-accent",
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
@@ -166,7 +162,7 @@ function DocSidebarNonCollapsible(props: {
|
||||
<div className="px-3">{name}</div>
|
||||
)}
|
||||
</div>
|
||||
<ul className="flex flex-col">
|
||||
<ul className="space-y-1">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
|
||||
@@ -207,7 +203,7 @@ function DocSidebarCategory(props: {
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 py-1.5 px-3 font-medium" ref={triggerRef}>
|
||||
<div className="flex gap-2 py-1.5 px-3" ref={triggerRef}>
|
||||
{icon && <SidebarIcon icon={icon} />}
|
||||
{name}
|
||||
</div>
|
||||
@@ -215,10 +211,7 @@ function DocSidebarCategory(props: {
|
||||
);
|
||||
|
||||
const triggerEl = href ? (
|
||||
<Link
|
||||
className={cn("w-full py-1.5 px-3 text-left font-medium")}
|
||||
href={href}
|
||||
>
|
||||
<Link className={cn("w-full py-1.5 px-3 text-left")} href={href}>
|
||||
{triggerElContent}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -233,16 +226,18 @@ function DocSidebarCategory(props: {
|
||||
trigger={triggerEl}
|
||||
triggerContainerClassName=""
|
||||
>
|
||||
<ul className="flex flex-col border-l-2 pl-4">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="pl-4 py-1">
|
||||
<ul className="flex flex-col border-l pl-2 gap-1">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</CustomAccordion>
|
||||
);
|
||||
}
|
||||
@@ -253,7 +248,7 @@ export function DocSidebarMobile(props: ReferenceSideBarProps) {
|
||||
return (
|
||||
<DropdownMenu onOpenChange={setOpen} open={open}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="w-full justify-between border bg-card py-3 text-left text-foreground hover:bg-card xl:hidden">
|
||||
<Button className="w-full justify-between border bg-card py-3 h-auto rounded-xl text-left text-foreground hover:bg-card xl:hidden">
|
||||
{props.name}
|
||||
<ChevronDownIcon
|
||||
className={clsx(
|
||||
@@ -265,7 +260,7 @@ export function DocSidebarMobile(props: ReferenceSideBarProps) {
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="center" asChild side="bottom" sideOffset={10}>
|
||||
<div className="max-h-[70vh] w-[calc(100vw-32px)] overflow-y-auto rounded-lg border bg-card px-4">
|
||||
<div className="max-h-[70vh] w-[calc(100vw-32px)] overflow-y-auto rounded-xl border bg-card px-2">
|
||||
<DocSidebar
|
||||
{...props}
|
||||
header={props.header}
|
||||
|
||||
@@ -35,12 +35,12 @@ export function Subscribe() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="mb-3 font-semibold text-base text-foreground">
|
||||
<p className="mb-2 text-sm text-muted-foreground text-right">
|
||||
Subscribe for the latest dev updates
|
||||
</p>
|
||||
<div className="flex">
|
||||
<Input
|
||||
className="h-12 border bg-background font-semibold duration-200 placeholder:font-semibold focus-visible:outline-none focus-visible:ring-offset-0 group-focus-within:border-foreground md:w-[230px]"
|
||||
className="rounded-r-none border-r-0 w-64"
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
@@ -53,14 +53,7 @@ export function Subscribe() {
|
||||
value={email}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="h-12 bg-muted font-semibold text-foreground duration-200 group-focus-within:bg-foreground group-focus-within:text-background"
|
||||
style={{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
<Button type="submit" variant="secondary" className="rounded-l-none">
|
||||
Subscribe
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -113,10 +113,14 @@ export function TableOfContentsSideBar(props: {
|
||||
};
|
||||
}, [filterHeading, pathname]);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
"shrink-0 hidden pt-6 text-sm xl:block",
|
||||
"shrink-0 hidden pt-6 text-sm xl:block animate-in fade-in-0",
|
||||
"styled-scrollbar sticky top-sticky-top-height h-sidebar-height flex-col overflow-y-auto",
|
||||
)}
|
||||
style={{
|
||||
@@ -124,7 +128,7 @@ export function TableOfContentsSideBar(props: {
|
||||
}}
|
||||
>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium mb-4 text-foreground border-b pb-4 text-base">
|
||||
<div className="font-medium mb-4 text-foreground text-base">
|
||||
On this page
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { HashIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Anchor(props: {
|
||||
@@ -25,7 +25,7 @@ export function Anchor(props: {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="size-4" />
|
||||
<HashIcon className="size-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const TabsList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
className={cn(
|
||||
"flex w-100 items-center justify-start border-b overflow-x-auto overflow-y-hidden whitespace-nowrap no-scrollbar",
|
||||
"flex w-full items-center justify-start border-b overflow-x-auto overflow-y-hidden whitespace-nowrap no-scrollbar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -28,10 +28,11 @@ const TabsTrigger = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn(
|
||||
"mb-[-2px] flex items-center whitespace-nowrap px-4 py-2 text-sm font-medium",
|
||||
"border-transparent border-b-4 text-muted-foreground ring-offset-700 transition-all",
|
||||
"data-[state=active]:border-violet-800 hover:text-foreground data-[state=active]:text-foreground",
|
||||
"mb-1 flex items-center whitespace-nowrap px-3 h-10 text-sm font-medium rounded-lg relative",
|
||||
"border-transparent border-b text-muted-foreground ring-offset-700 transition-all",
|
||||
"hover:text-foreground hover:bg-accent data-[state=active]:text-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
"data-[state=active]:before:opacity-100 before:absolute before:opacity-0 before:border-b-2 before:border-foreground before:left-0 before:right-0 before:bottom-[-5px] before:transition-opacity before:duration-300",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -14,6 +14,10 @@ const config: Config = {
|
||||
...tailwindConfig.theme,
|
||||
extend: {
|
||||
...tailwindConfig.theme?.extend,
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-sans)"],
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
},
|
||||
spacing: {
|
||||
"offset-top": "calc(var(--sticky-top-height) + 18px)",
|
||||
"offset-top-mobile": "calc(var(--sticky-top-height) + 100px)",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f152139`](https://github.com/thirdweb-dev/js/commit/f152139d739bbdfaefb07ee547c2655259c394b8)]:
|
||||
- thirdweb@5.108.3
|
||||
|
||||
## 0.2.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f7cda6`](https://github.com/thirdweb-dev/js/commit/1f7cda66988e88628ca9b8fe608af071aa0026a1), [`9069351`](https://github.com/thirdweb-dev/js/commit/9069351e1abd231cf9051b05b7d92f95f891060b)]:
|
||||
- thirdweb@5.108.2
|
||||
|
||||
## 0.2.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.54"
|
||||
"version": "0.2.56"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.108.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8128](https://github.com/thirdweb-dev/js/pull/8128) [`f152139`](https://github.com/thirdweb-dev/js/commit/f152139d739bbdfaefb07ee547c2655259c394b8) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Support ERC-2612 permit for x402 payments
|
||||
|
||||
## 5.108.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8106](https://github.com/thirdweb-dev/js/pull/8106) [`1f7cda6`](https://github.com/thirdweb-dev/js/commit/1f7cda66988e88628ca9b8fe608af071aa0026a1) Thanks [@MananTank](https://github.com/MananTank)! - Add `BridgeWidget` component.
|
||||
|
||||
Generate a browser script in `dist/scripts/bridge-widget.js` that can be used to render the `BridgeWidget` component in a browser with a script
|
||||
|
||||
- [#8126](https://github.com/thirdweb-dev/js/pull/8126) [`9069351`](https://github.com/thirdweb-dev/js/commit/9069351e1abd231cf9051b05b7d92f95f891060b) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Accept chain objects for x402 APIs
|
||||
|
||||
## 5.108.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"@types/prompts": "2.4.9",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@viem/anvil": "0.0.10",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
@@ -89,6 +90,7 @@
|
||||
"sharp": "^0.34.2",
|
||||
"size-limit": "11.2.0",
|
||||
"storybook": "9.0.15",
|
||||
"tsup": "^8.5.0",
|
||||
"typedoc": "0.27.9",
|
||||
"typedoc-better-json": "0.9.4",
|
||||
"typescript": "5.8.3",
|
||||
@@ -323,7 +325,8 @@
|
||||
"scripts": {
|
||||
"bench": "vitest -c ./test/vitest.config.ts bench",
|
||||
"bench:compare": "bun run ./benchmarks/run.ts",
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm",
|
||||
"build": "pnpm clean && pnpm build:types && pnpm build:cjs && pnpm build:esm && pnpm build:tsup",
|
||||
"build:tsup": "tsup",
|
||||
"build-storybook": "storybook build",
|
||||
"build:cjs": "tsc --noCheck --project ./tsconfig.build.json --module commonjs --outDir ./dist/cjs --verbatimModuleSyntax false && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json",
|
||||
"build:esm": "tsc --noCheck --project ./tsconfig.build.json --module es2020 --outDir ./dist/esm && printf '{\"type\": \"module\",\"sideEffects\":false}' > ./dist/esm/package.json",
|
||||
@@ -427,5 +430,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.108.1"
|
||||
"version": "5.108.3"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature)"
|
||||
]
|
||||
@@ -24,9 +24,4 @@
|
||||
"function getSessionStateForSigner(address signer) view returns (((uint256 remaining, address target, bytes4 selector, uint256 index)[] transferValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callValue, (uint256 remaining, address target, bytes4 selector, uint256 index)[] callParams))",
|
||||
"function getTransferPoliciesForSigner(address signer) view returns ((address target, uint256 maxValuePerUse, (uint8 limitType, uint256 limit, uint256 period) valueLimit)[])",
|
||||
"function isWildcardSigner(address signer) view returns (bool)",
|
||||
"function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) returns (bytes4)",
|
||||
"function onERC1155Received(address, address, uint256, uint256, bytes) returns (bytes4)",
|
||||
"function onERC721Received(address, address, uint256, bytes) returns (bytes4)",
|
||||
"function supportsInterface(bytes4 interfaceId) view returns (bool)",
|
||||
"receive() external payable"
|
||||
]
|
||||
@@ -9,7 +9,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { PreparedQuote, Quote } from "./types/Quote.js";
|
||||
|
||||
/**
|
||||
* Retrieves a Universal Bridge quote for the provided buy intent. The quote will specify the necessary `originAmount` to receive the desired `destinationAmount`, which is specified with the `buyAmountWei` option.
|
||||
* Retrieves a Bridge quote for the provided buy intent. The quote will specify the necessary `originAmount` to receive the desired `destinationAmount`, which is specified with the `buyAmountWei` option.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -209,7 +209,7 @@ export declare namespace quote {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a **finalized** Universal Bridge quote for the provided buy request with transaction data. This function will return everything `quote` does, with the addition of a series of prepared transactions and the associated expiration timestamp.
|
||||
* Prepares a **finalized** Bridge quote for the provided buy request with transaction data. This function will return everything `quote` does, with the addition of a series of prepared transactions and the associated expiration timestamp.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Chain } from "./types/Chain.js";
|
||||
import { ApiError } from "./types/Errors.js";
|
||||
|
||||
/**
|
||||
* Retrieves supported Universal Bridge chains.
|
||||
* Retrieves supported Bridge chains.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { Route } from "./types/Route.js";
|
||||
|
||||
/**
|
||||
* Retrieves supported Universal Bridge routes based on the provided filters.
|
||||
* Retrieves supported Bridge routes based on the provided filters.
|
||||
*
|
||||
* When multiple filters are specified, a route must satisfy all filters to be included (it acts as an AND operator).
|
||||
*
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { PreparedQuote, Quote } from "./types/Quote.js";
|
||||
|
||||
/**
|
||||
* Retrieves a Universal Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
|
||||
* Retrieves a Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -200,7 +200,7 @@ export declare namespace quote {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a **finalized** Universal Bridge quote for the provided sell request with transaction data. This function will return everything `quote` does, with the addition of a series of prepared transactions and the associated expiration timestamp.
|
||||
* Prepares a **finalized** Bridge quote for the provided sell request with transaction data. This function will return everything `quote` does, with the addition of a series of prepared transactions and the associated expiration timestamp.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { Status } from "./types/Status.js";
|
||||
|
||||
/**
|
||||
* Retrieves a Universal Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
|
||||
* Retrieves a Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
|
||||
*
|
||||
* The returned status will include both the origin and destination transactions and any finalized amounts for the route.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { Token, TokenWithPrices } from "./types/Token.js";
|
||||
|
||||
/**
|
||||
* Retrieves supported Universal Bridge tokens based on the provided filters.
|
||||
* Retrieves supported Bridge tokens based on the provided filters.
|
||||
*
|
||||
* When multiple filters are specified, a token must satisfy all filters to be included (it acts as an AND operator).
|
||||
*
|
||||
@@ -226,9 +226,9 @@ export declare namespace tokens {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a token to the Universal Bridge for indexing.
|
||||
* Adds a token to the Bridge for indexing.
|
||||
*
|
||||
* This function requests the Universal Bridge to index a specific token on a given chain.
|
||||
* This function requests the Bridge to index a specific token on a given chain.
|
||||
* Once indexed, the token will be available for cross-chain operations.
|
||||
*
|
||||
* @example
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ApiError } from "./types/Errors.js";
|
||||
import type { PreparedQuote } from "./types/Quote.js";
|
||||
|
||||
/**
|
||||
* Prepares a **finalized** Universal Bridge quote for the provided transfer request with transaction data.
|
||||
* Prepares a **finalized** Bridge quote for the provided transfer request with transaction data.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
|
||||
@@ -90,7 +90,7 @@ export type WebhookPayload = Exclude<
|
||||
>;
|
||||
|
||||
/**
|
||||
* Parses an incoming Universal Bridge webhook payload.
|
||||
* Parses an incoming Bridge webhook payload.
|
||||
*
|
||||
* @param payload - The raw text body received from thirdweb.
|
||||
* @param headers - The webhook headers received from thirdweb.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Represents a blockchain chain in the Universal Bridge.
|
||||
* Represents a blockchain chain in the Bridge.
|
||||
* @public
|
||||
*/
|
||||
export interface Chain {
|
||||
|
||||
@@ -137,6 +137,10 @@ export {
|
||||
BuyWidget,
|
||||
type BuyWidgetProps,
|
||||
} from "../react/web/ui/Bridge/BuyWidget.js";
|
||||
export {
|
||||
BridgeWidget,
|
||||
type BridgeWidgetProps,
|
||||
} from "../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
|
||||
export {
|
||||
CheckoutWidget,
|
||||
type CheckoutWidgetProps,
|
||||
|
||||
@@ -6,7 +6,7 @@ declare module "abitype" {
|
||||
}
|
||||
|
||||
/**
|
||||
* UNIVERSAL BRIDGE
|
||||
* BRIDGE
|
||||
*/
|
||||
export * as Bridge from "../bridge/index.js";
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { decodePayment, encodePayment } from "../x402/encode.js";
|
||||
export {
|
||||
facilitator,
|
||||
type ThirdwebX402Facilitator,
|
||||
type ThirdwebX402FacilitatorConfig,
|
||||
} from "../x402/facilitator.js";
|
||||
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";
|
||||
|
||||
Generated
+215
@@ -0,0 +1,215 @@
|
||||
import type { AbiParameterToPrimitiveType } from "abitype";
|
||||
import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js";
|
||||
import type {
|
||||
BaseTransactionOptions,
|
||||
WithOverrides,
|
||||
} from "../../../../../transaction/types.js";
|
||||
import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
|
||||
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
|
||||
import { once } from "../../../../../utils/promise/once.js";
|
||||
|
||||
/**
|
||||
* Represents the parameters for the "transferWithAuthorization" function.
|
||||
*/
|
||||
export type TransferWithAuthorizationParams = WithOverrides<{
|
||||
from: AbiParameterToPrimitiveType<{ type: "address"; name: "from" }>;
|
||||
to: AbiParameterToPrimitiveType<{ type: "address"; name: "to" }>;
|
||||
value: AbiParameterToPrimitiveType<{ type: "uint256"; name: "value" }>;
|
||||
validAfter: AbiParameterToPrimitiveType<{
|
||||
type: "uint256";
|
||||
name: "validAfter";
|
||||
}>;
|
||||
validBefore: AbiParameterToPrimitiveType<{
|
||||
type: "uint256";
|
||||
name: "validBefore";
|
||||
}>;
|
||||
nonce: AbiParameterToPrimitiveType<{ type: "bytes32"; name: "nonce" }>;
|
||||
signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>;
|
||||
}>;
|
||||
|
||||
export const FN_SELECTOR = "0xcf092995" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
type: "address",
|
||||
name: "from",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
name: "to",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "value",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "validAfter",
|
||||
},
|
||||
{
|
||||
type: "uint256",
|
||||
name: "validBefore",
|
||||
},
|
||||
{
|
||||
type: "bytes32",
|
||||
name: "nonce",
|
||||
},
|
||||
{
|
||||
type: "bytes",
|
||||
name: "signature",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [] as const;
|
||||
|
||||
/**
|
||||
* Checks if the `transferWithAuthorization` method is supported by the given contract.
|
||||
* @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
|
||||
* @returns A boolean indicating if the `transferWithAuthorization` method is supported.
|
||||
* @extension ERC20
|
||||
* @example
|
||||
* ```ts
|
||||
* import { isTransferWithAuthorizationSupported } from "thirdweb/extensions/erc20";
|
||||
*
|
||||
* const supported = isTransferWithAuthorizationSupported(["0x..."]);
|
||||
* ```
|
||||
*/
|
||||
export function isTransferWithAuthorizationSupported(
|
||||
availableSelectors: string[],
|
||||
) {
|
||||
return detectMethod({
|
||||
availableSelectors,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the parameters for the "transferWithAuthorization" function.
|
||||
* @param options - The options for the transferWithAuthorization function.
|
||||
* @returns The encoded ABI parameters.
|
||||
* @extension ERC20
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeTransferWithAuthorizationParams } from "thirdweb/extensions/erc20";
|
||||
* const result = encodeTransferWithAuthorizationParams({
|
||||
* from: ...,
|
||||
* to: ...,
|
||||
* value: ...,
|
||||
* validAfter: ...,
|
||||
* validBefore: ...,
|
||||
* nonce: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeTransferWithAuthorizationParams(
|
||||
options: TransferWithAuthorizationParams,
|
||||
) {
|
||||
return encodeAbiParameters(FN_INPUTS, [
|
||||
options.from,
|
||||
options.to,
|
||||
options.value,
|
||||
options.validAfter,
|
||||
options.validBefore,
|
||||
options.nonce,
|
||||
options.signature,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the "transferWithAuthorization" function into a Hex string with its parameters.
|
||||
* @param options - The options for the transferWithAuthorization function.
|
||||
* @returns The encoded hexadecimal string.
|
||||
* @extension ERC20
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encodeTransferWithAuthorization } from "thirdweb/extensions/erc20";
|
||||
* const result = encodeTransferWithAuthorization({
|
||||
* from: ...,
|
||||
* to: ...,
|
||||
* value: ...,
|
||||
* validAfter: ...,
|
||||
* validBefore: ...,
|
||||
* nonce: ...,
|
||||
* signature: ...,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function encodeTransferWithAuthorization(
|
||||
options: TransferWithAuthorizationParams,
|
||||
) {
|
||||
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
|
||||
// we can do this because we know the specific formats of the values
|
||||
return (FN_SELECTOR +
|
||||
encodeTransferWithAuthorizationParams(options).slice(
|
||||
2,
|
||||
)) as `${typeof FN_SELECTOR}${string}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a transaction to call the "transferWithAuthorization" function on the contract.
|
||||
* @param options - The options for the "transferWithAuthorization" function.
|
||||
* @returns A prepared transaction object.
|
||||
* @extension ERC20
|
||||
* @example
|
||||
* ```ts
|
||||
* import { sendTransaction } from "thirdweb";
|
||||
* import { transferWithAuthorization } from "thirdweb/extensions/erc20";
|
||||
*
|
||||
* const transaction = transferWithAuthorization({
|
||||
* contract,
|
||||
* from: ...,
|
||||
* to: ...,
|
||||
* value: ...,
|
||||
* validAfter: ...,
|
||||
* validBefore: ...,
|
||||
* nonce: ...,
|
||||
* signature: ...,
|
||||
* overrides: {
|
||||
* ...
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Send the transaction
|
||||
* await sendTransaction({ transaction, account });
|
||||
* ```
|
||||
*/
|
||||
export function transferWithAuthorization(
|
||||
options: BaseTransactionOptions<
|
||||
| TransferWithAuthorizationParams
|
||||
| {
|
||||
asyncParams: () => Promise<TransferWithAuthorizationParams>;
|
||||
}
|
||||
>,
|
||||
) {
|
||||
const asyncOptions = once(async () => {
|
||||
return "asyncParams" in options ? await options.asyncParams() : options;
|
||||
});
|
||||
|
||||
return prepareContractCall({
|
||||
contract: options.contract,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
params: async () => {
|
||||
const resolvedOptions = await asyncOptions();
|
||||
return [
|
||||
resolvedOptions.from,
|
||||
resolvedOptions.to,
|
||||
resolvedOptions.value,
|
||||
resolvedOptions.validAfter,
|
||||
resolvedOptions.validBefore,
|
||||
resolvedOptions.nonce,
|
||||
resolvedOptions.signature,
|
||||
] as const;
|
||||
},
|
||||
value: async () => (await asyncOptions()).overrides?.value,
|
||||
accessList: async () => (await asyncOptions()).overrides?.accessList,
|
||||
gas: async () => (await asyncOptions()).overrides?.gas,
|
||||
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
|
||||
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
|
||||
maxPriorityFeePerGas: async () =>
|
||||
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
|
||||
nonce: async () => (await asyncOptions()).overrides?.nonce,
|
||||
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
|
||||
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
|
||||
authorizationList: async () =>
|
||||
(await asyncOptions()).overrides?.authorizationList,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { status as OnrampStatus } from "../../../bridge/OnrampStatus.js";
|
||||
import { ApiError } from "../../../bridge/types/Errors.js";
|
||||
@@ -10,14 +9,9 @@ import type { Status } from "../../../bridge/types/Status.js";
|
||||
import { getCachedChain } from "../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { waitForReceipt } from "../../../transaction/actions/wait-for-tx-receipt.js";
|
||||
import { stringify } from "../../../utils/json.js";
|
||||
import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js";
|
||||
import type { WindowAdapter } from "../adapters/WindowAdapter.js";
|
||||
import {
|
||||
type BridgePrepareRequest,
|
||||
type BridgePrepareResult,
|
||||
useBridgePrepare,
|
||||
} from "./useBridgePrepare.js";
|
||||
import type { BridgePrepareResult } from "./useBridgePrepare.js";
|
||||
|
||||
/**
|
||||
* Type for completed status results from Bridge.status and Onramp.status
|
||||
@@ -35,8 +29,7 @@ export type CompletedStatusResult =
|
||||
* Options for the step executor hook
|
||||
*/
|
||||
interface StepExecutorOptions {
|
||||
/** Prepared quote returned by Bridge.prepare */
|
||||
request: BridgePrepareRequest;
|
||||
preparedQuote: BridgePrepareResult;
|
||||
/** Wallet instance providing getAccount() & sendTransaction */
|
||||
wallet?: Wallet;
|
||||
/** Window adapter for opening on-ramp URLs (web / RN) */
|
||||
@@ -67,7 +60,7 @@ interface StepExecutorResult {
|
||||
currentTxIndex?: number;
|
||||
progress: number; // 0–100
|
||||
onrampStatus?: "pending" | "executing" | "completed" | "failed";
|
||||
executionState: "fetching" | "idle" | "executing" | "auto-starting";
|
||||
executionState: "idle" | "executing" | "auto-starting";
|
||||
steps?: RouteStep[];
|
||||
error?: ApiError;
|
||||
start: () => void;
|
||||
@@ -100,16 +93,14 @@ export function useStepExecutor(
|
||||
options: StepExecutorOptions,
|
||||
): StepExecutorResult {
|
||||
const {
|
||||
request,
|
||||
wallet,
|
||||
windowAdapter,
|
||||
client,
|
||||
autoStart = false,
|
||||
onComplete,
|
||||
preparedQuote,
|
||||
} = options;
|
||||
|
||||
const { data: preparedQuote, isLoading } = useBridgePrepare(request);
|
||||
|
||||
// Flatten all transactions upfront
|
||||
const flatTxs = useMemo(
|
||||
() => (preparedQuote?.steps ? flattenRouteSteps(preparedQuote.steps) : []),
|
||||
@@ -121,7 +112,7 @@ export function useStepExecutor(
|
||||
undefined,
|
||||
);
|
||||
const [executionState, setExecutionState] = useState<
|
||||
"fetching" | "idle" | "executing" | "auto-starting"
|
||||
"idle" | "executing" | "auto-starting"
|
||||
>("idle");
|
||||
const [error, setError] = useState<ApiError | undefined>(undefined);
|
||||
const [completedTxs, setCompletedTxs] = useState<Set<number>>(new Set());
|
||||
@@ -129,22 +120,6 @@ export function useStepExecutor(
|
||||
"pending" | "executing" | "completed" | "failed" | undefined
|
||||
>(preparedQuote?.type === "onramp" ? "pending" : undefined);
|
||||
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
if (!isLoading) {
|
||||
setExecutionState("idle");
|
||||
} else {
|
||||
setExecutionState("fetching");
|
||||
}
|
||||
return executionState;
|
||||
},
|
||||
queryKey: [
|
||||
"bridge-quote-execution-state",
|
||||
stringify(preparedQuote?.steps),
|
||||
isLoading,
|
||||
],
|
||||
});
|
||||
|
||||
// Cancellation tracking
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ interface UseTransactionDetailsOptions {
|
||||
transaction: PreparedTransaction;
|
||||
client: ThirdwebClient;
|
||||
wallet: Wallet | undefined;
|
||||
currency?: SupportedFiatCurrency;
|
||||
currency: SupportedFiatCurrency | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -189,13 +189,9 @@ export function BridgeOrchestrator({
|
||||
}, [send, uiOptions.mode]);
|
||||
|
||||
// Handle post-buy transaction completion
|
||||
const handlePostBuyTransactionComplete = useCallback(
|
||||
(quote: BridgePrepareResult) => {
|
||||
onComplete?.(quote);
|
||||
send({ type: "RESET" });
|
||||
},
|
||||
[onComplete, send],
|
||||
);
|
||||
const handlePostBuyTransactionComplete = useCallback(() => {
|
||||
send({ type: "RESET" });
|
||||
}, [send]);
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback(
|
||||
@@ -379,6 +375,7 @@ export function BridgeOrchestrator({
|
||||
|
||||
{state.value === "execute" && quote && state.context.request && (
|
||||
<StepRunner
|
||||
preparedQuote={quote}
|
||||
autoStart={true}
|
||||
client={client}
|
||||
onBack={() => {
|
||||
@@ -415,9 +412,9 @@ export function BridgeOrchestrator({
|
||||
quote &&
|
||||
uiOptions.transaction && (
|
||||
<ExecutingTxScreen
|
||||
closeModal={() => handlePostBuyTransactionComplete(quote)}
|
||||
closeModal={handlePostBuyTransactionComplete}
|
||||
onTxSent={() => {
|
||||
// Do nothing
|
||||
onComplete?.(quote);
|
||||
}}
|
||||
tx={uiOptions.transaction}
|
||||
windowAdapter={webWindowAdapter}
|
||||
|
||||
@@ -33,7 +33,7 @@ import type { LocaleId } from "../types.js";
|
||||
import { BridgeOrchestrator, type UIOptions } from "./BridgeOrchestrator.js";
|
||||
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
|
||||
|
||||
type BuyOrOnrampPrepareResult = Extract<
|
||||
export type BuyOrOnrampPrepareResult = Extract<
|
||||
BridgePrepareResult,
|
||||
{ type: "buy" | "onramp" }
|
||||
>;
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
radius,
|
||||
spacing,
|
||||
} from "../../../core/design-system/index.js";
|
||||
import type { BridgePrepareRequest } from "../../../core/hooks/useBridgePrepare.js";
|
||||
import type {
|
||||
BridgePrepareRequest,
|
||||
BridgePrepareResult,
|
||||
} from "../../../core/hooks/useBridgePrepare.js";
|
||||
import {
|
||||
type CompletedStatusResult,
|
||||
useStepExecutor,
|
||||
@@ -62,6 +65,11 @@ interface StepRunnerProps {
|
||||
* Called when user clicks the back button
|
||||
*/
|
||||
onBack?: () => void;
|
||||
|
||||
/**
|
||||
* Prepared quote to use
|
||||
*/
|
||||
preparedQuote: BridgePrepareResult;
|
||||
}
|
||||
|
||||
export function StepRunner({
|
||||
@@ -74,6 +82,7 @@ export function StepRunner({
|
||||
onCancel,
|
||||
onBack,
|
||||
autoStart,
|
||||
preparedQuote,
|
||||
}: StepRunnerProps) {
|
||||
const theme = useCustomTheme();
|
||||
|
||||
@@ -94,8 +103,8 @@ export function StepRunner({
|
||||
onComplete: (completedStatuses: CompletedStatusResult[]) => {
|
||||
onComplete(completedStatuses);
|
||||
},
|
||||
request,
|
||||
wallet,
|
||||
preparedQuote,
|
||||
windowAdapter,
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ export function TransactionPayment({
|
||||
client,
|
||||
transaction: uiOptions.transaction,
|
||||
wallet,
|
||||
currency: uiOptions.currency,
|
||||
});
|
||||
|
||||
// We can't use useWalletBalance here because erc20Value is a possibly async value
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { useState } from "react";
|
||||
import { defineChain } from "../../../../../chains/utils.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
|
||||
import type { PurchaseData } from "../../../../../pay/types.js";
|
||||
import {
|
||||
CustomThemeProvider,
|
||||
useCustomTheme,
|
||||
} from "../../../../core/design-system/CustomThemeProvider.js";
|
||||
import {
|
||||
fontSize,
|
||||
radius,
|
||||
spacing,
|
||||
type Theme,
|
||||
} from "../../../../core/design-system/index.js";
|
||||
import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js";
|
||||
import { Container } from "../../components/basic.js";
|
||||
import { Button } from "../../components/buttons.js";
|
||||
import { type BuyOrOnrampPrepareResult, BuyWidget } from "../BuyWidget.js";
|
||||
import { SwapWidget } from "../swap-widget/SwapWidget.js";
|
||||
import type { SwapPreparedQuote } from "../swap-widget/types.js";
|
||||
|
||||
/**
|
||||
* Props for the `BridgeWidget` component.
|
||||
*/
|
||||
export type BridgeWidgetProps = {
|
||||
/**
|
||||
* A client is the entry point to the thirdweb SDK. It is required for all other actions.
|
||||
* You can create a client using the `createThirdwebClient` function. Refer to the
|
||||
* [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation for more information.
|
||||
*
|
||||
* You must provide a `clientId` or `secretKey` in order to initialize a client. Pass `clientId` for client-side usage and `secretKey` for server-side usage.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createThirdwebClient } from "thirdweb";
|
||||
*
|
||||
* const client = createThirdwebClient({
|
||||
* clientId: "<your_client_id>",
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
client: ThirdwebClient;
|
||||
|
||||
/**
|
||||
* Set the theme for the widget. By default it is set to `"dark"`.
|
||||
*
|
||||
* Theme can be set to either `"dark"`, `"light"` or a custom theme object.
|
||||
* You can also import [`lightTheme`](https://portal.thirdweb.com/references/typescript/v5/lightTheme)
|
||||
* or [`darkTheme`](https://portal.thirdweb.com/references/typescript/v5/darkTheme) from `thirdweb/react`
|
||||
* to use the default themes as base and override parts of it.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { lightTheme } from "thirdweb/react";
|
||||
*
|
||||
* const customTheme = lightTheme({
|
||||
* colors: { modalBg: "red" },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
theme?: "light" | "dark" | Theme;
|
||||
|
||||
/**
|
||||
* Whether to show thirdweb branding in the widget.
|
||||
* @default true
|
||||
*/
|
||||
showThirdwebBranding?: boolean;
|
||||
|
||||
/**
|
||||
* The currency to use for fiat pricing in the widget.
|
||||
* @default "USD"
|
||||
*/
|
||||
currency?: SupportedFiatCurrency;
|
||||
|
||||
/**
|
||||
* Configuration for the Swap tab. This mirrors {@link SwapWidget} options where applicable.
|
||||
*/
|
||||
swap?: {
|
||||
/** Optional class name applied to the Swap tab content container. */
|
||||
className?: string;
|
||||
/** Optional style overrides applied to the Swap tab content container. */
|
||||
style?: React.CSSProperties;
|
||||
/** Callback invoked when a swap is successful. */
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
/** Callback invoked when an error occurs during swapping. */
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
/** Callback invoked when the user cancels the swap. */
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
/** Callback invoked when the user disconnects the active wallet. */
|
||||
onDisconnect?: () => void;
|
||||
/**
|
||||
* Whether to persist token selections to localStorage so that revisits pre-select last used tokens.
|
||||
* Prefill values take precedence over persisted selections.
|
||||
* @default true
|
||||
*/
|
||||
persistTokenSelections?: boolean;
|
||||
/**
|
||||
* Prefill initial buy/sell token selections. If `tokenAddress` is not provided, the native token will be used.
|
||||
*
|
||||
* @example
|
||||
* ### Set an ERC20 token as the buy token
|
||||
* ```tsx
|
||||
* <BridgeWidget
|
||||
* client={client}
|
||||
* buy={{ amount: "0.1", chainId: 8453 }}
|
||||
* swap={{
|
||||
* prefill: {
|
||||
* buyToken: {
|
||||
* chainId: 8453,
|
||||
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
* },
|
||||
* },
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Set a native token as the sell token
|
||||
* ```tsx
|
||||
* <BridgeWidget
|
||||
* client={client}
|
||||
* buy={{ amount: "0.1", chainId: 8453 }}
|
||||
* swap={{
|
||||
* prefill: {
|
||||
* sellToken: { chainId: 8453 },
|
||||
* },
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
prefill?: {
|
||||
/** Buy token selection. If `tokenAddress` is omitted, the native token will be used. */
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
/** Optional human-readable amount to prefill for buy. */
|
||||
amount?: string;
|
||||
};
|
||||
/** Sell token selection. If `tokenAddress` is omitted, the native token will be used. */
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
/** Optional human-readable amount to prefill for sell. */
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for the Buy tab. This mirrors {@link BuyWidget} options where applicable.
|
||||
*/
|
||||
buy: {
|
||||
/**
|
||||
* The amount to buy (as a decimal string), e.g. "1.5" for 1.5 tokens.
|
||||
*/
|
||||
amount: string; // TODO - make it optional
|
||||
/**
|
||||
* The chain the accepted token is on.
|
||||
*/
|
||||
chainId: number; // TODO - make it optional
|
||||
/**
|
||||
* Address of the token to buy. Leave undefined for the native token, or use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
|
||||
*/
|
||||
tokenAddress?: string;
|
||||
/** Custom label for the main action button. */
|
||||
buttonLabel?: string;
|
||||
/** Callback triggered when the user cancels the purchase. */
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
/** Callback triggered when the purchase encounters an error. */
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
/** Callback triggered when the purchase is successful. */
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
/** Optional class name applied to the Buy tab content container. */
|
||||
className?: string;
|
||||
/** The user's ISO 3166 alpha-2 country code. Used to determine onramp provider support. */
|
||||
country?: string;
|
||||
/** Preset fiat amounts to display in the UI. Defaults to [5, 10, 20]. */
|
||||
presetOptions?: [number, number, number];
|
||||
/** Arbitrary data to be included in returned status and webhook events. */
|
||||
purchaseData?: PurchaseData;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A combined widget for swapping or buying tokens with cross-chain support.
|
||||
*
|
||||
* This component renders two tabs – "Swap" and "Buy" – and orchestrates the appropriate flow
|
||||
* by composing {@link SwapWidget} and {@link BuyWidget} under the hood.
|
||||
*
|
||||
* - The Swap tab enables token-to-token swaps (including cross-chain).
|
||||
* - The Buy tab enables purchasing a specific token; by default, it uses card onramp in this widget.
|
||||
*
|
||||
* @param props - Props of type {@link BridgeWidgetProps} to configure the BridgeWidget component.
|
||||
*
|
||||
* @example
|
||||
* ### Basic usage
|
||||
* ```tsx
|
||||
* <BridgeWidget
|
||||
* client={client}
|
||||
* currency="USD"
|
||||
* theme="dark"
|
||||
* showThirdwebBranding
|
||||
* buy={{
|
||||
* // Buy 0.1 native tokens on Base
|
||||
* amount: "0.1",
|
||||
* chainId: 8453,
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* ### Prefill swap tokens and configure buy
|
||||
* ```tsx
|
||||
* <BridgeWidget
|
||||
* client={client}
|
||||
* swap={{
|
||||
* prefill: {
|
||||
* buyToken: {
|
||||
* // Base USDC
|
||||
* chainId: 8453,
|
||||
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
* },
|
||||
* sellToken: {
|
||||
* // Polygon native token (MATIC)
|
||||
* chainId: 137,
|
||||
* },
|
||||
* },
|
||||
* }}
|
||||
* buy={{
|
||||
* amount: "100",
|
||||
* chainId: 8453,
|
||||
* tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
* buttonLabel: "Buy USDC",
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @bridge
|
||||
*/
|
||||
export function BridgeWidget(props: BridgeWidgetProps) {
|
||||
const [tab, setTab] = useState<"swap" | "buy">("swap");
|
||||
|
||||
return (
|
||||
<CustomThemeProvider theme={props.theme}>
|
||||
<EmbedContainer
|
||||
modalSize="compact"
|
||||
style={{
|
||||
borderRadius: radius.xl,
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
px="md"
|
||||
py="md"
|
||||
flex="row"
|
||||
gap="xs"
|
||||
borderColor="borderColor"
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: "dashed",
|
||||
}}
|
||||
>
|
||||
<TabButton isActive={tab === "swap"} onClick={() => setTab("swap")}>
|
||||
Swap
|
||||
</TabButton>
|
||||
<TabButton isActive={tab === "buy"} onClick={() => setTab("buy")}>
|
||||
Buy
|
||||
</TabButton>
|
||||
</Container>
|
||||
|
||||
{tab === "swap" && (
|
||||
<SwapWidget
|
||||
client={props.client}
|
||||
prefill={props.swap?.prefill}
|
||||
className={props.swap?.className}
|
||||
showThirdwebBranding={props.showThirdwebBranding}
|
||||
currency={props.currency}
|
||||
theme={props.theme}
|
||||
onSuccess={props.swap?.onSuccess}
|
||||
onError={props.swap?.onError}
|
||||
onCancel={props.swap?.onCancel}
|
||||
onDisconnect={props.swap?.onDisconnect}
|
||||
persistTokenSelections={props.swap?.persistTokenSelections}
|
||||
style={{
|
||||
border: "none",
|
||||
...props.swap?.style,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tab === "buy" && (
|
||||
<BuyWidget
|
||||
client={props.client}
|
||||
amount={props.buy.amount}
|
||||
showThirdwebBranding={props.showThirdwebBranding}
|
||||
chain={defineChain(props.buy.chainId)}
|
||||
currency={props.currency}
|
||||
theme={props.theme}
|
||||
title="" // Keep it empty string to hide the title
|
||||
tokenAddress={props.buy.tokenAddress as `0x${string}` | undefined}
|
||||
buttonLabel={props.buy.buttonLabel}
|
||||
className={props.buy.className}
|
||||
country={props.buy.country}
|
||||
onCancel={props.buy.onCancel}
|
||||
onError={props.buy.onError}
|
||||
onSuccess={props.buy.onSuccess}
|
||||
presetOptions={props.buy.presetOptions}
|
||||
purchaseData={props.buy.purchaseData}
|
||||
paymentMethods={["card"]}
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EmbedContainer>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton(props: {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const theme = useCustomTheme();
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={props.onClick}
|
||||
style={{
|
||||
borderRadius: radius.full,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: 500,
|
||||
paddingInline: spacing["md+"],
|
||||
paddingBlock: spacing.sm,
|
||||
border: `1px solid ${
|
||||
props.isActive ? theme.colors.secondaryText : theme.colors.borderColor
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -210,6 +210,7 @@ const TransactionOverViewCompact = (props: {
|
||||
client: props.client,
|
||||
transaction: props.uiOptions.transaction,
|
||||
wallet: props.paymentMethod.payerWallet,
|
||||
currency: props.uiOptions.currency,
|
||||
});
|
||||
|
||||
if (!txInfo.data) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
|
||||
import type { Buy, Sell } from "../../../../../bridge/index.js";
|
||||
import type { TokenWithPrices } from "../../../../../bridge/types/Token.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
@@ -46,7 +48,7 @@ export type SwapWidgetProps = {
|
||||
*/
|
||||
client: ThirdwebClient;
|
||||
/**
|
||||
* The prefill Buy and/or Sell tokens for the swap widget. If `tokenAddress` is not provided, the native token will be used
|
||||
* Prefill Buy and/or Sell tokens for the swap widget. If `tokenAddress` is not provided, the native token will be used
|
||||
*
|
||||
* @example
|
||||
*
|
||||
@@ -241,6 +243,17 @@ export type SwapWidgetProps = {
|
||||
* @bridge
|
||||
*/
|
||||
export function SwapWidget(props: SwapWidgetProps) {
|
||||
useQuery({
|
||||
queryFn: () => {
|
||||
trackPayEvent({
|
||||
client: props.client,
|
||||
event: "ub:ui:swap_widget:render",
|
||||
});
|
||||
return true;
|
||||
},
|
||||
queryKey: ["swap_widget:render"],
|
||||
});
|
||||
|
||||
return (
|
||||
<SwapWidgetContainer
|
||||
theme={props.theme}
|
||||
@@ -435,6 +448,7 @@ function SwapWidgetContent(props: SwapWidgetProps) {
|
||||
<StepRunner
|
||||
title="Processing Swap"
|
||||
autoStart={true}
|
||||
preparedQuote={screen.preparedQuote}
|
||||
client={props.client}
|
||||
onBack={() => {
|
||||
setScreen({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Chain as BridgeChain } from "../../../../../bridge/index.js";
|
||||
import type { ThirdwebClient } from "../../../../../client/client.js";
|
||||
import {
|
||||
@@ -53,7 +53,22 @@ export function SelectBridgeChainUI(
|
||||
},
|
||||
) {
|
||||
const [search, setSearch] = useState("");
|
||||
const filteredChains = props.chains.filter((chain) => {
|
||||
const [initiallySelectedChain] = useState(props.selectedChain);
|
||||
|
||||
// put the initially selected chain first
|
||||
const sortedChains = useMemo(() => {
|
||||
if (initiallySelectedChain) {
|
||||
return [
|
||||
initiallySelectedChain,
|
||||
...props.chains.filter(
|
||||
(chain) => chain.chainId !== initiallySelectedChain.chainId,
|
||||
),
|
||||
];
|
||||
}
|
||||
return props.chains;
|
||||
}, [props.chains, initiallySelectedChain]);
|
||||
|
||||
const filteredChains = sortedChains.filter((chain) => {
|
||||
return chain.name.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { tokenAmountFormatter } from "./utils.js";
|
||||
* @internal
|
||||
*/
|
||||
type SelectTokenUIProps = {
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
client: ThirdwebClient;
|
||||
selectedToken: TokenSelection | undefined;
|
||||
setSelectedToken: (token: TokenSelection) => void;
|
||||
@@ -182,6 +182,7 @@ function SelectTokenUI(
|
||||
});
|
||||
}, [otherTokens]);
|
||||
|
||||
// desktop
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<Container
|
||||
@@ -205,7 +206,10 @@ function SelectTokenUI(
|
||||
</LeftContainer>
|
||||
<Container flex="column" relative scrollY>
|
||||
<TokenSelectionScreen
|
||||
onSelectToken={props.setSelectedToken}
|
||||
onSelectToken={(token) => {
|
||||
props.setSelectedToken(token);
|
||||
props.onClose();
|
||||
}}
|
||||
isMobile={false}
|
||||
selectedToken={props.selectedToken}
|
||||
isFetching={props.isFetching}
|
||||
@@ -226,7 +230,10 @@ function SelectTokenUI(
|
||||
if (screen === "select-token") {
|
||||
return (
|
||||
<TokenSelectionScreen
|
||||
onSelectToken={props.setSelectedToken}
|
||||
onSelectToken={(token) => {
|
||||
props.setSelectedToken(token);
|
||||
props.onClose();
|
||||
}}
|
||||
selectedToken={props.selectedToken}
|
||||
isFetching={props.isFetching}
|
||||
ownedTokens={props.ownedTokens}
|
||||
@@ -581,8 +588,10 @@ function TokenSelectionScreen(props: {
|
||||
client={props.client}
|
||||
onSelect={props.onSelectToken}
|
||||
isSelected={
|
||||
props.selectedToken?.tokenAddress.toLowerCase() ===
|
||||
token.address.toLowerCase()
|
||||
!!props.selectedToken &&
|
||||
props.selectedToken.tokenAddress.toLowerCase() ===
|
||||
token.address.toLowerCase() &&
|
||||
token.chainId === props.selectedToken.chainId
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -235,20 +235,16 @@ export function SwapUI(props: SwapUIProps) {
|
||||
{modalState.screen === "select-buy-token" && (
|
||||
<SelectToken
|
||||
activeWalletInfo={props.activeWalletInfo}
|
||||
onBack={() =>
|
||||
setModalState((v) => ({
|
||||
...v,
|
||||
isOpen: false,
|
||||
}))
|
||||
}
|
||||
client={props.client}
|
||||
selectedToken={props.buyToken}
|
||||
setSelectedToken={(token) => {
|
||||
props.setBuyToken(token);
|
||||
onClose={() => {
|
||||
setModalState((v) => ({
|
||||
...v,
|
||||
isOpen: false,
|
||||
}));
|
||||
}}
|
||||
client={props.client}
|
||||
selectedToken={props.buyToken}
|
||||
setSelectedToken={(token) => {
|
||||
props.setBuyToken(token);
|
||||
// if buy token is same as sell token, unset sell token
|
||||
if (
|
||||
props.sellToken &&
|
||||
@@ -264,20 +260,16 @@ export function SwapUI(props: SwapUIProps) {
|
||||
|
||||
{modalState.screen === "select-sell-token" && (
|
||||
<SelectToken
|
||||
onBack={() =>
|
||||
setModalState((v) => ({
|
||||
...v,
|
||||
isOpen: false,
|
||||
}))
|
||||
}
|
||||
client={props.client}
|
||||
selectedToken={props.sellToken}
|
||||
setSelectedToken={(token) => {
|
||||
props.setSellToken(token);
|
||||
onClose={() => {
|
||||
setModalState((v) => ({
|
||||
...v,
|
||||
isOpen: false,
|
||||
}));
|
||||
}}
|
||||
client={props.client}
|
||||
selectedToken={props.sellToken}
|
||||
setSelectedToken={(token) => {
|
||||
props.setSellToken(token);
|
||||
// if sell token is same as buy token, unset buy token
|
||||
if (
|
||||
props.buyToken &&
|
||||
|
||||
@@ -479,6 +479,8 @@ export const EmbedContainer = /* @__PURE__ */ StyledDiv<{
|
||||
lineHeight: "normal",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: modalSize === "compact" ? modalMaxWidthCompact : modalMaxWidthWide,
|
||||
width: "100vw",
|
||||
maxWidth:
|
||||
modalSize === "compact" ? modalMaxWidthCompact : modalMaxWidthWide,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createThirdwebClient } from "../client/client.js";
|
||||
import type { SupportedFiatCurrency } from "../pay/convert/type.js";
|
||||
import type { PurchaseData } from "../pay/types.js";
|
||||
import {
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
type Theme,
|
||||
type ThemeOverrides,
|
||||
} from "../react/core/design-system/index.js";
|
||||
import type { BuyOrOnrampPrepareResult } from "../react/web/ui/Bridge/BuyWidget.js";
|
||||
import { BridgeWidget } from "../react/web/ui/Bridge/bridge-widget/bridge-widget.js";
|
||||
import type { SwapPreparedQuote } from "../react/web/ui/Bridge/swap-widget/types.js";
|
||||
|
||||
// Note: do not use SwapWidgetProps or BuyWidgetProps references here to keep the output for bridge-widget.d.ts as simple as possible
|
||||
// Note: these props will be configured buy user in a <script> tag, so they need to be as simple as possible and can not rely on utils like `darkTheme`, `createThirdwebClient` etc..
|
||||
// For example, instead of having for a `Chain` prop, use `chainId` instead
|
||||
|
||||
export type BridgeWidgetScriptProps = {
|
||||
clientId: string;
|
||||
theme?: "light" | "dark" | ({ type: "light" | "dark" } & ThemeOverrides);
|
||||
showThirdwebBranding?: boolean;
|
||||
currency?: SupportedFiatCurrency;
|
||||
swap?: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
buy: {
|
||||
amount: string; // TODO - make it optional
|
||||
chainId: number; // TODO - make it optional
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
purchaseData?: PurchaseData;
|
||||
};
|
||||
};
|
||||
|
||||
export function BridgeWidgetScript(props: BridgeWidgetScriptProps) {
|
||||
const client = createThirdwebClient({ clientId: props.clientId });
|
||||
const themeObj: Theme | "light" | "dark" | undefined =
|
||||
typeof props.theme === "object"
|
||||
? props.theme.type === "dark"
|
||||
? darkTheme(props.theme)
|
||||
: lightTheme(props.theme)
|
||||
: props.theme;
|
||||
|
||||
return <BridgeWidget {...props} theme={themeObj} client={client} />;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type Container, createRoot } from "react-dom/client";
|
||||
import { ThirdwebProvider } from "../react/web/providers/thirdweb-provider.js";
|
||||
import {
|
||||
BridgeWidgetScript,
|
||||
type BridgeWidgetScriptProps,
|
||||
} from "./bridge-widget-script.js";
|
||||
|
||||
// Note: This file is built as a UMD module with globalName "BridgeWidget"
|
||||
// This will be available as a global function called `BridgeWidget.render`
|
||||
|
||||
export function render(element: Container, props: BridgeWidgetScriptProps) {
|
||||
createRoot(element).render(
|
||||
<ThirdwebProvider>
|
||||
<BridgeWidgetScript {...props} />
|
||||
</ThirdwebProvider>,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# Bridge Widget
|
||||
|
||||
Add the script in document head and the element where you want to render the bridge widget in your page
|
||||
|
||||
```html
|
||||
<!-- add the bridge-widget script in the head of the document, it adds the `BridgeWidget` global variable to the window object -->
|
||||
<script src="https://unpkg.com/thirdweb/scripts/bridge-widget.js"></script>
|
||||
|
||||
<!-- add a unique id to an element where you want to render the bridge widget -->
|
||||
<div id="bridge-widget"></div>
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<script>
|
||||
// get the element where you want to render the bridge widget
|
||||
const node = document.getElementById("bridge-widget");
|
||||
|
||||
// render the widget
|
||||
BridgeWidget.render(node, {
|
||||
clientId: "your-client-id",
|
||||
theme: "dark",
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Custom Theme
|
||||
|
||||
```html
|
||||
<script>
|
||||
BridgeWidget.render(node, {
|
||||
clientId: "your-client-id",
|
||||
theme: {
|
||||
type: "dark",
|
||||
colors: {
|
||||
modalBg: "red",
|
||||
},
|
||||
},
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Customizing Swap UI
|
||||
|
||||
```html
|
||||
<script>
|
||||
BridgeWidget.render(node, {
|
||||
clientId: "your-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -241,6 +241,37 @@ export const TransactionLight: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Transaction mode in light theme showing an ERC20 token transfer.
|
||||
*/
|
||||
export const TransactionJPYCurrency: Story = {
|
||||
args: {
|
||||
connectLocale: undefined,
|
||||
connectOptions: undefined,
|
||||
onCancel: undefined,
|
||||
onComplete: undefined,
|
||||
onError: undefined,
|
||||
paymentLinkId: undefined,
|
||||
presetOptions: undefined,
|
||||
purchaseData: undefined,
|
||||
receiverAddress: undefined,
|
||||
theme: "light",
|
||||
uiOptions: {
|
||||
...TRANSACTION_UI_OPTIONS.erc20Transfer,
|
||||
currency: "JPY",
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
backgrounds: { default: "light" },
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Light theme version of transaction mode showing an ERC20 token transfer with proper token amount formatting and USD conversion.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustompresetOptions: Story = {
|
||||
args: {
|
||||
connectLocale: undefined,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta } from "@storybook/react";
|
||||
import { BridgeWidgetScript } from "../../../script-exports/bridge-widget-script.js";
|
||||
import { storyClient } from "../../utils.js";
|
||||
|
||||
const meta: Meta<typeof BridgeWidgetScript> = {
|
||||
title: "Bridge/BridgeWidgetScript",
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div>
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export function BasicUsage() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
clientId={storyClient.clientId}
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LightTheme() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
clientId={storyClient.clientId}
|
||||
theme="light"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CurrencySet() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
clientId={storyClient.clientId}
|
||||
currency="JPY"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoThirdwebBranding() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
clientId={storyClient.clientId}
|
||||
theme="light"
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
showThirdwebBranding={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CustomTheme() {
|
||||
return (
|
||||
<BridgeWidgetScript
|
||||
clientId={storyClient.clientId}
|
||||
buy={{ chainId: 8453, amount: "0.1" }}
|
||||
theme={{
|
||||
type: "light",
|
||||
colors: {
|
||||
modalBg: "#FFFFF0",
|
||||
tertiaryBg: "#DBE4C9",
|
||||
borderColor: "#8AA624",
|
||||
secondaryText: "#3E3F29",
|
||||
accentText: "#E43636",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import type { CompletedStatusResult } from "../../react/core/hooks/useStepExecut
|
||||
import { StepRunner } from "../../react/web/ui/Bridge/StepRunner.js";
|
||||
import type { Wallet } from "../../wallets/interfaces/wallet.js";
|
||||
import { ModalThemeWrapper, storyClient } from "../utils.js";
|
||||
import { STORY_MOCK_WALLET, simpleBuyRequest } from "./fixtures.js";
|
||||
import {
|
||||
STORY_MOCK_WALLET,
|
||||
simpleBuyQuote,
|
||||
simpleBuyRequest,
|
||||
} from "./fixtures.js";
|
||||
|
||||
// Mock window adapter
|
||||
const mockWindowAdapter: WindowAdapter = {
|
||||
@@ -32,7 +36,7 @@ const StepRunnerWithTheme = (props: StepRunnerWithThemeProps) => {
|
||||
const { theme, ...componentProps } = props;
|
||||
return (
|
||||
<ModalThemeWrapper theme={theme}>
|
||||
<StepRunner {...componentProps} />
|
||||
<StepRunner {...componentProps} preparedQuote={simpleBuyQuote} />
|
||||
</ModalThemeWrapper>
|
||||
);
|
||||
};
|
||||
@@ -59,29 +63,8 @@ const meta = {
|
||||
},
|
||||
component: StepRunnerWithTheme,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"**StepRunner** executes prepared route steps sequentially, showing real-time progress and transaction status.\n\n" +
|
||||
"## Features\n" +
|
||||
"- **Real Execution**: Uses useStepExecutor hook for actual transaction processing\n" +
|
||||
"- **Progress Tracking**: Visual progress bar and step-by-step status updates\n" +
|
||||
"- **Error Handling**: Retry functionality for failed transactions\n" +
|
||||
"- **Transaction Batching**: Optimizes multiple transactions when possible\n" +
|
||||
"- **Onramp Support**: Handles fiat-to-crypto onramp flows\n\n" +
|
||||
"## Props\n" +
|
||||
"- `steps`: Array of RouteStep objects from Bridge.prepare()\n" +
|
||||
"- `wallet`: Connected wallet for transaction signing\n" +
|
||||
"- `client`: ThirdwebClient instance\n" +
|
||||
"- `windowAdapter`: Platform-specific window/URL handler\n" +
|
||||
"- `onramp`: Optional onramp configuration\n\n" +
|
||||
"## Integration\n" +
|
||||
"This component is typically used within the BridgeOrchestrator after route preparation.",
|
||||
},
|
||||
},
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
title: "Bridge/StepRunner",
|
||||
} satisfies Meta<typeof StepRunnerWithTheme>;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export const TokenNotSupported: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Shows the loading state when a token is being indexed by the Universal Bridge on a mainnet chain.",
|
||||
"Shows the loading state when a token is being indexed by the Bridge on a mainnet chain.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -85,7 +85,7 @@ export const TokenNotSupportedLight: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Shows the loading state when a token is being indexed by the Universal Bridge on a mainnet chain (light theme).",
|
||||
"Shows the loading state when a token is being indexed by the Bridge on a mainnet chain (light theme).",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,7 +102,7 @@ export const TestnetNotSupported: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Shows the error state when trying to use the Universal Bridge on a testnet chain (Sepolia in this example).",
|
||||
"Shows the error state when trying to use the Bridge on a testnet chain (Sepolia in this example).",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -119,7 +119,7 @@ export const TestnetNotSupportedLight: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Shows the error state when trying to use the Universal Bridge on a testnet chain (Sepolia in this example, light theme).",
|
||||
"Shows the error state when trying to use the Bridge on a testnet chain (Sepolia in this example, light theme).",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ type DomainOverrides = {
|
||||
*/
|
||||
engineCloud?: string;
|
||||
/**
|
||||
* The base URL for the universal bridge service.
|
||||
* The base URL for the bridge service.
|
||||
* @default "bridge.thirdweb.com"
|
||||
*/
|
||||
bridge?: string;
|
||||
|
||||
@@ -199,6 +199,19 @@ describe("toUnits", () => {
|
||||
expect(toUnits("69.59000000059", 9)).toMatchInlineSnapshot("69590000001n");
|
||||
expect(toUnits("69.59000002359", 9)).toMatchInlineSnapshot("69590000024n");
|
||||
});
|
||||
|
||||
it("scientific notation", () => {
|
||||
// negative exponents
|
||||
expect(toUnits("1e-18", 18)).toMatchInlineSnapshot("1n");
|
||||
expect(toUnits("1e-16", 18)).toMatchInlineSnapshot("100n");
|
||||
expect(toUnits("1.23456789012345e-7", 18)).toMatchInlineSnapshot(
|
||||
"123456789012n",
|
||||
);
|
||||
// positive exponents
|
||||
expect(toUnits("1.234e3", 18)).toMatchInlineSnapshot(
|
||||
"1234000000000000000000n",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toWei", () => {
|
||||
|
||||
@@ -73,6 +73,10 @@ export function toEther(wei: bigint) {
|
||||
* @utils
|
||||
*/
|
||||
export function toUnits(tokens: string, decimals: number): bigint {
|
||||
if (tokens.includes("e")) {
|
||||
tokens = Number(tokens).toFixed(decimals);
|
||||
}
|
||||
|
||||
let [integerPart, fractionPart = ""] = tokens.split(".") as [string, string];
|
||||
const prefix = integerPart.startsWith("-") ? "-" : "";
|
||||
if (prefix) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user