Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fb8c95a325 Version Packages (#8130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 19:24:09 -07:00
Joaquim VergesandGitHub cf02cbf3af [wagmi-adapter] Add onConnect callback (#8131) 2025-09-25 19:00:35 -07:00
Joaquim VergesandGitHub f152139d73 [SDK] Support ERC-2612 permit for x402 payments (#8128) 2025-09-25 18:21:58 -07:00
MananTank 16307019be Portal: Add BridgeWidget script docs (#8129)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `BridgeWidget` functionality, updating UI components, and improving documentation for better user experience and clarity.

### Detailed summary
- Added `BridgeWidget Script` route in sidebar.
- Updated `Paragraph` and `Heading` components' margin classes.
- Revised `Details` component structure.
- Changed text references from `BuyWidget` to `SwapWidget`.
- Enhanced documentation for `BridgeWidget Script` with detailed integration instructions.
- Introduced new options for customizing the widget's appearance and behavior.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-25 22:50:37 +00:00
MananTank 10fb338877 Portal: UI tweaks 2 (#8127)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces several enhancements and styling updates across various components in the application, including adjustments to CSS, layout changes, and the addition of a new package, `geist`. It aims to improve the overall appearance and functionality of the portal.

### Detailed summary
- Added `font-feature-settings` to `globals.css`.
- Updated `text-rendering`, `-webkit-font-smoothing`, and `text-size-adjust` in `global.css`.
- Included `geist` package in `package.json`.
- Modified layout and spacing in `vault/page.mdx`.
- Adjusted classes for `ApiEndpoint.tsx`, `Paragraph.tsx`, and `InlineCode.tsx`.
- Changed text color in `Subscribe.tsx`.
- Revised spacing and border styles in various components.
- Updated icons in `Anchor.tsx` and `ChatButton.tsx`.
- Enhanced styling of buttons and links across multiple components.
- Improved overall layout structure in `Header.tsx` and sidebar components.
- Refined search modal styles and item presentation in `DocSearch.tsx`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- Style
  - Switched site typography to Geist, enabled ligatures and improved text rendering.
  - Redesigned header with central branding, consolidated controls, mobile menu and subtle backdrop.
  - Refreshed visuals across search, footer, sidebar, platform selector, steps, cards, code/inline code, and mobile sidebar.
  - Updated icons (chat, next-page, anchors), spacing/typography tweaks; vault features now stack vertically.

- Refactor
  - Localized an article card component and reorganized footer/next-page flows; Edit link now wrapped in a button.

- Chores
  - Added Geist dependency and extended Tailwind font settings; global font/rendering tweaks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 22:50:36 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
95412c75b9 Version Packages (#8114)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 10:12:22 -07:00
MananTank 7bc513c770 [BLD-328] Portal: Update changelog page layout (#8124)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on UI improvements and code refactoring across various components in the application, enhancing styling consistency, accessibility, and overall user experience.

### Detailed summary
- Updated grid spacing in `Grid.tsx`.
- Increased max-width in `layout.tsx`.
- Modified border styles in multiple components.
- Enhanced button styles and variants in `chat-button.tsx` and `Subscribe.tsx`.
- Improved text styles for better readability.
- Refactored `AuthMethodsTabs.tsx` to use `Button` component.
- Adjusted `DocLayout.tsx` for more responsive design.
- Improved date formatting in `RenderData.tsx`.
- Added animations to `TableOfContents.tsx`.
- Updated feedback dialog in `Feedback.tsx` for better UX.
- General styling updates for consistency across components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- Style
  - Widespread visual refinements: typography, spacing, tighter headings, dashed borders for dividers/images/code, smaller author avatar, absolute dates, wider content container, updated buttons (ghost/outline), adjusted icons and shadows, updated link/inline-code/grid/card styles, tabs and header/menu visual updates.
  - Footer, sidebar, docs search, subscribe, feedback, and table-of-contents presentational tweaks (including fade-in and conditional hiding).

- Refactor
  - Simplified changelog and post layout to a single-column header-first structure; removed changelog index TOC.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 08:00:13 +00:00
Joaquim VergesandGitHub 9069351e1a [SDK] Support Chain objects in x402 payment utilities (#8126) 2025-09-25 00:53:22 -07:00
MananTank 8f6363f4e4 SDK: Fix toUnits throwing error for values in scientific notation (#8112)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `toUnits` function to handle both scientific notation and fixed-point notation, ensuring accurate conversion of token values with specified decimals. It also adds tests to verify the new functionality.

### Detailed summary
- Modified `toUnits` function to check for scientific notation (e.g., "1e-18").
- Converted scientific notation to fixed-point notation using `Number(tokens).toFixed(decimals)`.
- Added tests for `toUnits` to cover various scientific notation cases, including negative and positive exponents.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Unit conversion now accepts scientific notation (positive and negative exponents), normalizing values to the configured decimal precision for consistent fixed-point results.

* **Tests**
  * Added comprehensive tests covering positive and negative exponent cases to validate parsing and conversion from scientific notation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 23:07:53 +00:00
MananTank a83104efff [MNY-200] SDK: Fix onSuccess callback not called when tx is sent in TransactionWidget (#8119)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR simplifies the `handlePostBuyTransactionComplete` function in the `BridgeOrchestrator` component by removing the `quote` parameter and the associated call to `onComplete`. It streamlines the transaction completion process and adjusts the `closeModal` prop accordingly.

### Detailed summary
- Removed the `quote` parameter from the `handlePostBuyTransactionComplete` function.
- Eliminated the call to `onComplete?.(quote)` within the `handlePostBuyTransactionComplete` function.
- Updated the `closeModal` prop in the `ExecutingTxScreen` component to directly use `handlePostBuyTransactionComplete` without arguments.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 7936ebb6e9 [MNY-180] SDK: Fix TransactionWidget not respecting currency prop (#8118)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of the `currency` property in various components and adding a new story for a transaction mode in light theme that showcases an ERC20 token transfer.

### Detailed summary
- Changed `currency` from optional to required in `useTransactionDetails.ts`.
- Added `currency` to the props in `PaymentOverview.tsx` and `TransactionPayment.tsx`.
- Introduced a new story `TransactionJPYCurrency` in `BridgeOrchestrator.stories.tsx` for displaying an ERC20 token transfer in light theme.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 3b986d52e5 [MNY-201] SDK: Rename leftover of Universal Bridge to Bridge (#8117)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming references from `Universal Bridge` to `Bridge` across multiple files to standardize terminology and improve clarity.

### Detailed summary
- Updated comments and documentation to replace `Universal Bridge` with `Bridge`.
- Modified error messages in `apps/dashboard/src/@/api/analytics.ts` to reflect the new terminology.
- Changed descriptions in various files related to `Chain`, `Token`, `Quote`, and webhooks to use `Bridge`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank ee4e13e098 [MNY-198] SDK: Fix StepRunner autoStart not working reliably, Remove extra prepare call (#8116)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `StepRunner` component in the Bridge UI by integrating a `preparedQuote` prop, improving the handling of swap transactions, and refining the relevant hooks and stories.

### Detailed summary
- Added `preparedQuote` prop to `SwapWidget` and `StepRunner`.
- Updated `StepRunner` to utilize `preparedQuote` for executing steps.
- Modified `useStepExecutor` to accept `preparedQuote` instead of `request`.
- Cleaned up imports in `useStepExecutor`.
- Updated stories to reflect the new `preparedQuote` prop.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 8dc9e4102d [MNY-197] SDK: SwapWidget chain selection UI improvements (#8115)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the user experience in the `SwapUI` and `SelectTokenUI` components by updating callback functions and enhancing token selection logic.

### Detailed summary
- Changed `onBack` prop to `onClose` in `SelectToken` and `SelectTokenUI` components.
- Updated modal state management to close modals on token selection.
- Introduced `initiallySelectedChain` to prioritize the selected chain in `SelectBridgeChainUI`.
- Enhanced filtering logic for token selection to consider chain IDs.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 33fc3e5226 [MNY-196] SDK: Add SwapWidget render event (#8113)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new query using `useQuery` from `@tanstack/react-query` in the `SwapWidget` component to track the rendering event of the swap widget.

### Detailed summary
- Added `useQuery` to the `SwapWidget` component.
- Implemented a query function that calls `trackPayEvent` to log the rendering of the swap widget.
- The tracking event includes the `client` from `props` and an event identifier `"ub:ui:swap_widget:render"`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Chores**
  * Added background telemetry for the Swap Widget to record render events for analytics. It runs automatically when the widget is displayed, requires no configuration, and does not change the UI or user experience. This helps track usage and adoption to inform future improvements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:29:54 +00:00
MananTank ed47e93f1d [BLD-338] Dashboard: Fix NFTs table owner row not showing data (#8120)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a temporary change to the `table.tsx` file in the NFTs component of the dashboard. It sets the `useIndexer` property to `false`, indicating a workaround until a future fix is implemented.

### Detailed summary
- Added `useIndexer: false` to the settings in `table.tsx`.
- Marked the addition as temporary with a comment: `// TEMPORARY, TODO: Remove when this is fixed in insight`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved reliability of NFT lists in the dashboard, reducing intermittent errors and missing items when viewing NFTs for specific contracts.
  * Ensures more consistent, up-to-date NFT data is displayed during browsing and refreshes.
  * Enhances stability of data loading in environments with inconsistent indexing, minimizing failed loads and retries.
  * No visual/UI changes; behavior is more dependable without altering the interface or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:19:38 +00:00
MananTank 1f7cda6698 [MNY-210] SDK: export a script to render BridgeEmbed (#8106)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `BridgeWidget` component, allowing users to render a widget for token buying and swapping. It also updates related files for better integration and testing, while modifying build configurations and dependencies.

### Detailed summary
- Added `BridgeWidget` component.
- Created a browser script in `dist/scripts/bridge-widget.js`.
- Updated `BuyOrOnrampPrepareResult` type in `BuyWidget.tsx`.
- Excluded test files in `tsconfig.build.tsup.json`.
- Modified `tsup` configuration for building the widget.
- Enhanced documentation for the `BridgeWidget`.
- Added various usage examples in the README.
- Updated `pnpm-lock.yaml` with new dependencies and versions.

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a Bridge Widget with Swap and Buy tabs, theming, branding toggle, currency presets, and callback support.
  - Added an embeddable script API with a global render function to mount the Bridge Widget on any webpage and exposed it in React exports.

- **Style**
  - Connect Wallet embed now uses full-width with max-width constraints for responsive display.

- **Documentation**
  - Added usage guide and Storybook examples for the Bridge Widget script.

- **Build**
  - Added browser-targeted bundle config to produce a self-contained Bridge Widget script.

- **Chores**
  - Updated build scripts and dev dependencies.

- **Tests**
  - Skipped a batch transaction test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 17:30:52 +00:00
118 changed files with 2580 additions and 795 deletions
+2 -2
View File
@@ -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 [];
}
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
includeOwners: true,
start: currentPage * pageSize,
tokenByIndex,
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
},
);
@@ -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,
});
+31 -6
View File
@@ -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",
},
+1
View File
@@ -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",
+94 -96
View File
@@ -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`.
+13 -17
View File
@@ -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"
/>
);
}
+4
View File
@@ -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>
);
}
+41 -46
View File
@@ -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"
/>
);
}
}
+4
View File
@@ -1,3 +1,7 @@
html {
font-feature-settings: "liga" 1;
}
code span {
color: var(--code-light-color);
}
+3 -14
View File
@@ -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
+2 -1
View File
@@ -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>
);
}
+37 -37
View File
@@ -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>
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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" ? (
+1 -1
View File
@@ -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}
+32 -30
View File
@@ -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>
);
+26 -18
View File
@@ -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}
+25 -30
View File
@@ -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
+2 -2
View File
@@ -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>
+5 -4
View File
@@ -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}
+4
View File
@@ -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)",
+14
View File
@@ -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
+1 -1
View File
@@ -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"
}
+16
View File
@@ -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
+5 -2
View File
@@ -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"
]
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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).
*
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
*
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
/**
* Represents a blockchain chain in the Universal Bridge.
* Represents a blockchain chain in the Bridge.
* @public
*/
export interface Chain {
+4
View File
@@ -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,
+1 -1
View File
@@ -6,7 +6,7 @@ declare module "abitype" {
}
/**
* UNIVERSAL BRIDGE
* BRIDGE
*/
export * as Bridge from "../bridge/index.js";
/**
+1
View File
@@ -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";
@@ -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).",
},
},
},
+1 -1
View File
@@ -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;
+13
View File
@@ -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", () => {
+4
View File
@@ -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