[MNY-226] Add Bridge Widget playgrounds (script, iframe, react) (#8571)

<!--

## 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` component within the playground by introducing new features, improving styling, and refining existing functionalities. It also includes updates to metadata and the handling of token selections and amounts.

### Detailed summary
- Removed `PayIcon` and replaced with `BringToFrontIcon`.
- Added `showThirdwebBranding` option to `UniversalBridgeEmbed`.
- Updated `BridgeWidget` to support token amounts and selections.
- Enhanced sorting of bridge chains in `chains.ts`.
- Improved styling for various components, including `FeatureCard` and `ChatPageContent`.
- Updated documentation for widget parameters.
- Refactored code for better readability and maintainability in multiple 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

* **New Features**
  * Bridge Widget playground: interactive preview, code generation (script/component/iframe), theme, token/amount prefills, and preview modes.
  * Unified buy/swap experience under a single Bridge widget and a new branding toggle (hide/show branding).
  * Expanded connection options for wallet/connect flows.

* **Documentation**
  * Added docs for token amounts, branding, and iframe examples.

* **Style**
  * Minor typography and spacing refinements.

* **Chores**
  * Navigation and feature card updates; small test adjustment (one image test skipped).

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
MananTank
2025-12-16 20:42:47 +00:00
parent 4bab6f1ae8
commit aad3e6477e
25 changed files with 1341 additions and 288 deletions
@@ -2,13 +2,8 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useMemo, useRef, useState } from "react";
import { defineChain } from "thirdweb";
import {
BuyWidget,
type SupportedFiatCurrency,
SwapWidget,
} from "thirdweb/react";
import { useEffect, useMemo, useRef } from "react";
import { BridgeWidget, type SupportedFiatCurrency } from "thirdweb/react";
import type { Wallet } from "thirdweb/wallets";
import {
reportAssetBuyCancelled,
@@ -22,14 +17,12 @@ import {
reportTokenSwapFailed,
reportTokenSwapSuccessful,
} from "@/analytics/report";
import { Button } from "@/components/ui/button";
import {
NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID,
NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID,
NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID,
NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID,
} from "@/constants/public-envs";
import { cn } from "@/lib/utils";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { appMetadata } from "../../constants/connect";
@@ -71,11 +64,11 @@ export type BuyAndSwapEmbedProps = {
pageType: PageType;
wallets?: Wallet[];
currency?: SupportedFiatCurrency;
showThirdwebBranding?: boolean;
};
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
const { theme } = useTheme();
const [tab, setTab] = useState<"buy" | "swap">("swap");
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
const isMounted = useRef(false);
@@ -108,211 +101,161 @@ export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
}, [props.pageType]);
return (
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl min-w-0">
<div className="flex gap-2.5 p-4 border-b border-dashed">
<TabButton
label="Swap"
onClick={() => setTab("swap")}
isActive={tab === "swap"}
/>
<TabButton
label="Buy"
onClick={() => setTab("buy")}
isActive={tab === "buy"}
/>
</div>
<BridgeWidget
client={client}
theme={themeObj}
className="z-10 min-w-0 shadow-xl"
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
buy={
props.buyTab
? {
amount: props.buyTab.buyToken?.amount,
chainId: props.buyTab.buyToken?.chainId,
tokenAddress: props.buyTab.buyToken?.tokenAddress,
onError: (e, quote) => {
const errorMessage = parseError(e);
{tab === "buy" && (
<BuyWidget
currency={props.currency || "USD"}
amount={props.buyTab?.buyToken?.amount || "1"}
chain={
props.buyTab?.buyToken?.chainId
? defineChain(props.buyTab.buyToken.chainId)
: undefined
}
tokenAddress={
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
}
className="!rounded-2xl !border-none"
title=""
client={client}
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
onError={(e, quote) => {
const errorMessage = parseError(e);
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
if (!buyChainId) {
return;
reportTokenBuyFailed({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyFailed({
assetType: "coin",
chainId: buyChainId,
error: errorMessage,
contractType: undefined,
is_testnet: false,
});
}
},
onCancel: (quote) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuyCancelled({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyCancelled({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
},
onSuccess: ({ quote }) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuySuccessful({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuySuccessful({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
},
}
reportTokenBuyFailed({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyFailed({
assetType: "coin",
chainId: buyChainId,
error: errorMessage,
contractType: undefined,
is_testnet: false,
});
}
}}
onCancel={(quote) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuyCancelled({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyCancelled({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
}}
onSuccess={({ quote }) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuySuccessful({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuySuccessful({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
}}
theme={themeObj}
paymentMethods={["card"]}
/>
)}
{tab === "swap" && (
<SwapWidget
currency={props.currency || "USD"}
persistTokenSelections={props.persistTokenSelections}
client={client}
theme={themeObj}
className="!rounded-2xl !border-none"
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
prefill={{
buyToken: props.swapTab?.buyToken,
sellToken: props.swapTab?.sellToken,
}}
onError={(error, quote) => {
const errorMessage = parseError(error);
reportTokenSwapFailed({
errorMessage: errorMessage,
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
onSuccess={({ quote }) => {
reportTokenSwapSuccessful({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
onCancel={(quote) => {
reportTokenSwapCancelled({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
/>
)}
</div>
);
}
function TabButton(props: {
label: string;
onClick: () => void;
isActive: boolean;
}) {
return (
<Button
onClick={props.onClick}
size="sm"
className={cn(
"rounded-full text-muted-foreground px-4 bg-accent",
props.isActive && "text-foreground border-foreground/50",
)}
variant="outline"
>
{props.label}
</Button>
: undefined
}
swap={{
persistTokenSelections: props.persistTokenSelections,
prefill: {
buyToken: props.swapTab?.buyToken,
sellToken: props.swapTab?.sellToken,
},
onError: (error, quote) => {
const errorMessage = parseError(error);
reportTokenSwapFailed({
errorMessage: errorMessage,
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
onSuccess: ({ quote }) => {
reportTokenSwapSuccessful({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
onCancel: (quote) => {
reportTokenSwapCancelled({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
}}
currency={props.currency}
showThirdwebBranding={props.showThirdwebBranding}
/>
);
}
@@ -283,7 +283,7 @@ function TokenInfoSection(props: {
<dt className="text-xs font-medium text-muted-foreground tracking-wider uppercase">
{props.label}
</dt>
<dd className="text-3xl font-bold text-foreground tracking-tight">
<dd className="text-3xl font-semibold text-foreground tracking-tight">
{props.value}
</dd>
</dl>
@@ -25,6 +25,7 @@ export function UniversalBridgeEmbed(props: {
swapTab: BuyAndSwapEmbedProps["swapTab"];
pageType: "bridge" | "bridge-iframe";
currency?: SupportedFiatCurrency;
showThirdwebBranding?: boolean;
}) {
return (
<BuyAndSwapEmbed
@@ -34,6 +35,7 @@ export function UniversalBridgeEmbed(props: {
swapTab={props.swapTab}
pageType={props.pageType}
wallets={bridgeWallets}
showThirdwebBranding={props.showThirdwebBranding}
/>
);
}
@@ -35,9 +35,16 @@ export default async function Page(props: {
// output is buy, input is sell
const sellChain = parse(searchParams.inputChain, onlyNumber);
const sellCurrency = parse(searchParams.inputCurrency, onlyAddress);
const sellAmount = parse(searchParams.inputCurrencyAmount, onlyNumber);
const buyChain = parse(searchParams.outputChain, onlyNumber);
const buyCurrency = parse(searchParams.outputCurrency, onlyAddress);
const buyAmount = parse(searchParams.outputCurrencyAmount, onlyNumber);
const showThirdwebBranding = parse(
searchParams.showThirdwebBranding,
(v) => v !== "false",
);
const persistTokenSelections =
parse(searchParams.persistTokenSelections, (v) =>
@@ -61,11 +68,14 @@ export default async function Page(props: {
persistTokenSelections={persistTokenSelections === "true"}
pageType="bridge-iframe"
currency={currency}
showThirdwebBranding={showThirdwebBranding}
buyTab={{
buyToken: buyChain
? {
chainId: buyChain,
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
amount:
buyAmount === undefined ? undefined : buyAmount.toString(),
}
: undefined,
}}
@@ -74,12 +84,18 @@ export default async function Page(props: {
? {
chainId: sellChain,
tokenAddress: sellCurrency || NATIVE_TOKEN_ADDRESS,
amount:
sellAmount === undefined
? undefined
: sellAmount.toString(),
}
: undefined,
buyToken: buyChain
? {
chainId: buyChain,
tokenAddress: buyCurrency || NATIVE_TOKEN_ADDRESS,
amount:
buyAmount === undefined ? undefined : buyAmount.toString(),
}
: undefined,
}}
@@ -84,7 +84,7 @@ export function ChatPageContent(props: {
const userChainIdArray = userChainIds
.split(",")
.map((id) => id.trim())
.filter((id) => id !== "" && !isNaN(Number(id)));
.filter((id) => id !== "" && !Number.isNaN(Number(id)));
return {
chainIds:
@@ -0,0 +1,74 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { TabButtons } from "../../../../components/ui/tab-buttons";
import { LeftSection } from "./left-section";
import { RightSection } from "./right-section";
import type { BridgeWidgetPlaygroundOptions } from "./types";
const defaultOptions: BridgeWidgetPlaygroundOptions = {
integrationType: "iframe",
prefill: undefined,
currency: "USD",
showThirdwebBranding: true,
theme: {
darkColorOverrides: {},
lightColorOverrides: {},
type: "dark",
},
};
export function BridgeWidgetPlayground() {
const { theme } = useTheme();
const [options, setOptions] =
useState<BridgeWidgetPlaygroundOptions>(defaultOptions);
// change theme on global theme change
useEffect(() => {
setOptions((prev) => ({
...prev,
theme: {
...prev.theme,
type: theme === "dark" ? "dark" : "light",
},
}));
}, [theme]);
return (
<div>
<TabButtons
tabs={[
{
name: "Iframe",
onClick: () =>
setOptions({ ...options, integrationType: "iframe" }),
isActive: options.integrationType === "iframe",
},
{
name: "Script",
onClick: () =>
setOptions({ ...options, integrationType: "script" }),
isActive: options.integrationType === "script",
},
{
name: "React",
onClick: () =>
setOptions({ ...options, integrationType: "component" }),
isActive: options.integrationType === "component",
},
]}
/>
<div className="h-6" />
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<LeftSection options={options} setOptions={setOptions} />
</div>
<RightSection options={options} />
</div>
</div>
);
}
@@ -0,0 +1,72 @@
import type { BridgeWidgetPlaygroundOptions } from "./types";
const BRIDGE_WIDGET_IFRAME_BASE_URL = "https://thirdweb.com/bridge/widget";
export function buildIframeUrl(
options: BridgeWidgetPlaygroundOptions,
type: "code" | "preview",
) {
const url = new URL(BRIDGE_WIDGET_IFRAME_BASE_URL);
if (type === "preview") {
// always set it to false so playground doesn't show last used tokens
url.searchParams.set("persistTokenSelections", "false");
}
// Theme (only add if light, dark is default)
if (options.theme.type === "light") {
url.searchParams.set("theme", "light");
}
// Currency (only add if not USD, USD is default)
if (options.currency && options.currency !== "USD") {
url.searchParams.set("currency", options.currency);
}
// Branding
if (options.showThirdwebBranding === false) {
url.searchParams.set("showThirdwebBranding", "false");
}
// Sell token (input)
if (options.prefill?.sellToken) {
url.searchParams.set(
"inputChain",
String(options.prefill.sellToken.chainId),
);
if (options.prefill.sellToken.tokenAddress) {
url.searchParams.set(
"inputCurrency",
options.prefill.sellToken.tokenAddress,
);
}
if (options.prefill.sellToken.amount) {
url.searchParams.set(
"inputCurrencyAmount",
options.prefill.sellToken.amount,
);
}
}
// Buy token (output)
if (options.prefill?.buyToken) {
url.searchParams.set(
"outputChain",
String(options.prefill.buyToken.chainId),
);
if (options.prefill.buyToken.tokenAddress) {
url.searchParams.set(
"outputCurrency",
options.prefill.buyToken.tokenAddress,
);
}
if (options.prefill.buyToken.amount) {
url.searchParams.set(
"outputCurrencyAmount",
options.prefill.buyToken.amount,
);
}
}
return url.toString();
}
@@ -0,0 +1,214 @@
import { lazy, Suspense } from "react";
import { LoadingDots } from "@/components/ui/LoadingDots";
import { quotes, stringifyImports, stringifyProps } from "@/lib/code-gen";
import { buildIframeUrl } from "./buildIframeUrl";
import type { BridgeWidgetPlaygroundOptions } from "./types";
const CodeClient = lazy(() =>
import("../../../../components/code/code.client").then((m) => ({
default: m.CodeClient,
})),
);
function CodeLoading() {
return (
<div className="flex min-h-[300px] grow items-center justify-center bg-card border rounded-lg">
<LoadingDots />
</div>
);
}
export function CodeGen(props: { options: BridgeWidgetPlaygroundOptions }) {
return (
<div className="flex w-full grow flex-col">
<Suspense fallback={<CodeLoading />}>
<CodeClient
className="grow"
code={getCode(props.options)}
lang={props.options.integrationType === "component" ? "tsx" : "html"}
/>
</Suspense>
</div>
);
}
function getCode(options: BridgeWidgetPlaygroundOptions) {
if (options.integrationType === "script") {
return getCode_Script(options);
}
if (options.integrationType === "component") {
return getCode_ReactComponent(options);
}
if (options.integrationType === "iframe") {
return getCode_Iframe(options);
}
return "";
}
function getCode_Script(options: BridgeWidgetPlaygroundOptions) {
const widgetOptions: Record<string, unknown> = {
clientId: "your-thirdweb-client-id",
};
// Theme configuration
if (options.theme.type === "light") {
if (Object.keys(options.theme.lightColorOverrides || {}).length > 0) {
widgetOptions.theme = {
type: "light",
...options.theme.lightColorOverrides,
};
} else {
widgetOptions.theme = "light";
}
} else {
// dark theme
if (Object.keys(options.theme.darkColorOverrides || {}).length > 0) {
widgetOptions.theme = {
type: "dark",
...options.theme.darkColorOverrides,
};
}
// default is dark, so no need to set if no overrides
}
// Currency
if (options.currency && options.currency !== "USD") {
widgetOptions.currency = options.currency;
}
// Branding
if (options.showThirdwebBranding === false) {
widgetOptions.showThirdwebBranding = false;
}
// Buy tab options (for buyToken)
if (options.prefill?.buyToken) {
widgetOptions.buy = {
chainId: options.prefill.buyToken.chainId,
tokenAddress: options.prefill.buyToken.tokenAddress,
amount: options.prefill.buyToken.amount,
};
}
// Swap prefill options
if (options.prefill?.buyToken || options.prefill?.sellToken) {
widgetOptions.swap = {
prefill: options.prefill,
};
}
const optionsString = JSON.stringify(widgetOptions, null, 2)
.split("\n")
.map((line, i) => (i === 0 ? line : ` ${line}`))
.join("\n");
return `\
<!-- 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, ${optionsString});
</script>`;
}
function getCode_ReactComponent(options: BridgeWidgetPlaygroundOptions) {
const imports = {
thirdweb: ["createThirdwebClient"] as string[],
"thirdweb/react": ["BridgeWidget"] as string[],
};
let themeProp: string | undefined;
if (
options.theme.type === "dark" &&
Object.keys(options.theme.darkColorOverrides || {}).length > 0
) {
themeProp = `darkTheme({
colors: ${JSON.stringify(options.theme.darkColorOverrides)},
})`;
imports["thirdweb/react"].push("darkTheme");
}
if (options.theme.type === "light") {
if (Object.keys(options.theme.lightColorOverrides || {}).length > 0) {
themeProp = `lightTheme({
colors: ${JSON.stringify(options.theme.lightColorOverrides)},
})`;
imports["thirdweb/react"].push("lightTheme");
} else {
themeProp = quotes("light");
}
}
// Build buy prop for buyToken
let buyProp: string | undefined;
if (options.prefill?.buyToken) {
buyProp = JSON.stringify(
{
chainId: options.prefill.buyToken.chainId,
tokenAddress: options.prefill.buyToken.tokenAddress,
amount: options.prefill.buyToken.amount,
},
null,
2,
);
}
// Build swap prop with prefill
let swapProp: string | undefined;
if (options.prefill?.buyToken || options.prefill?.sellToken) {
swapProp = JSON.stringify(
{
prefill: {
buyToken: options.prefill?.buyToken,
sellToken: options.prefill?.sellToken,
},
},
null,
2,
);
}
const props: Record<string, string | undefined | boolean> = {
client: "client",
theme: themeProp,
buy: buyProp,
swap: swapProp,
currency:
options.currency !== "USD" && options.currency
? quotes(options.currency)
: undefined,
showThirdwebBranding:
options.showThirdwebBranding === false ? false : undefined,
};
return `\
${stringifyImports(imports)}
const client = createThirdwebClient({
clientId: "your-thirdweb-client-id",
});
function Example() {
return (
<BridgeWidget ${stringifyProps(props)} />
);
}`;
}
function getCode_Iframe(options: BridgeWidgetPlaygroundOptions) {
const src = buildIframeUrl(options, "code");
return `\
<iframe
src="${src}"
height="750px"
width="100%"
style="border: 0;"
/>`;
}
@@ -0,0 +1,232 @@
"use client";
import { CoinsIcon, PaletteIcon } from "lucide-react";
import type React from "react";
import { useId } from "react";
import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { CollapsibleSection } from "@/app/wallets/sign-in/components/CollapsibleSection";
import { ColorFormGroup } from "@/app/wallets/sign-in/components/ColorFormGroup";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TokenSelector } from "@/components/ui/TokenSelector";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { CurrencySelector } from "../../../../components/blocks/CurrencySelector";
import type { BridgeWidgetPlaygroundOptions } from "./types";
export function LeftSection(props: {
options: BridgeWidgetPlaygroundOptions;
setOptions: React.Dispatch<
React.SetStateAction<BridgeWidgetPlaygroundOptions>
>;
}) {
const { options, setOptions } = props;
const setThemeType = (themeType: "dark" | "light") => {
setOptions((v) => ({
...v,
theme: {
...v.theme,
type: themeType,
},
}));
};
const themeId = useId();
return (
<div className="flex flex-col gap-4">
<CollapsibleSection defaultOpen icon={CoinsIcon} title="Token Selection">
<div className="flex flex-col gap-6 pt-5">
<section className="flex flex-col gap-3">
<Label htmlFor="currency">Display Currency</Label>
<CurrencySelector
value={options.currency}
onChange={(currency) => {
setOptions((v) => ({ ...v, currency }));
}}
/>
</section>
<div className="border-t border-dashed" />
<TokenFieldset
title="Sell Token"
type="sellToken"
options={options}
setOptions={setOptions}
/>
<div className="border-t border-dashed" />
<TokenFieldset
title="Buy Token"
type="buyToken"
options={options}
setOptions={setOptions}
/>
</div>
</CollapsibleSection>
<CollapsibleSection icon={PaletteIcon} title="Appearance">
{/* Theme */}
<section className="flex flex-col gap-3 pt-6">
<Label htmlFor="theme"> Theme </Label>
<CustomRadioGroup
id={themeId}
onValueChange={setThemeType}
options={[
{ label: "Dark", value: "dark" },
{ label: "Light", value: "light" },
]}
value={options.theme.type}
/>
</section>
<div className="h-6" />
{/* Colors - disabled for iframe */}
{options.integrationType !== "iframe" && (
<ColorFormGroup
onChange={(newTheme) => {
setOptions((v) => ({
...v,
theme: newTheme,
}));
}}
theme={options.theme}
/>
)}
<div className="my-4 flex items-center gap-2">
<Checkbox
checked={options.showThirdwebBranding}
id={"branding"}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
showThirdwebBranding: checked === true,
}));
}}
/>
<Label htmlFor={"branding"}>Show Branding</Label>
</div>
</CollapsibleSection>
</div>
);
}
function TokenFieldset(props: {
type: "buyToken" | "sellToken";
title: string;
options: BridgeWidgetPlaygroundOptions;
setOptions: React.Dispatch<
React.SetStateAction<BridgeWidgetPlaygroundOptions>
>;
}) {
const { options, setOptions } = props;
const chainId = options.prefill?.[props.type]?.chainId;
const tokenAddress = options.prefill?.[props.type]?.tokenAddress;
return (
<div>
<h3 className="mb-1 font-medium">{props.title}</h3>
<p className="text-sm text-muted-foreground mb-3">
Sets the default token and amount to{" "}
{props.type === "buyToken" ? "buy" : "sell"} in the widget, <br />
User can change this default selection in the widget
</p>
<div className="space-y-4">
{/* Chain selection */}
<div className="space-y-2">
<Label>Chain</Label>
<BridgeNetworkSelector
chainId={chainId}
onChange={(chainId) => {
setOptions((v) => ({
...v,
prefill: {
...v.prefill,
[props.type]: {
...v.prefill?.[props.type],
chainId,
tokenAddress: undefined, // clear token selection
},
},
}));
}}
placeholder="Select a chain"
className="bg-card"
/>
</div>
{/* Token selection - only show if chain is selected */}
<div className="space-y-2">
<Label>Token</Label>
<TokenSelector
chainId={chainId}
client={THIRDWEB_CLIENT}
disableAddress
enabled={true}
onChange={(token) => {
setOptions((v) => ({
...v,
prefill: {
...v.prefill,
[props.type]: {
chainId: token.chainId,
tokenAddress: token.address,
},
},
}));
}}
placeholder="Select a token"
selectedToken={
tokenAddress && chainId
? {
address: tokenAddress,
chainId: chainId,
}
: chainId
? {
address: getAddress(NATIVE_TOKEN_ADDRESS),
chainId: chainId,
}
: undefined
}
className="bg-card"
/>
</div>
{chainId && (
<div className="space-y-2">
<Label> Token Amount</Label>
<Input
className="bg-card"
value={options.prefill?.[props.type]?.amount || ""}
onChange={(e) => {
setOptions((v) => {
return {
...v,
prefill: {
...v.prefill,
[props.type]: {
...v.prefill?.[props.type],
amount: e.target.value,
chainId: chainId,
},
},
};
});
}}
placeholder="0.01"
/>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import { useState } from "react";
import { BridgeWidget, darkTheme, lightTheme } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { cn } from "@/lib/utils";
import { buildIframeUrl } from "./buildIframeUrl";
import { CodeGen } from "./code";
import type { BridgeWidgetPlaygroundOptions } from "./types";
type Tab = "ui" | "code";
export function RightSection(props: {
options: BridgeWidgetPlaygroundOptions;
}) {
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
return "ui";
});
function setPreviewTab(tab: "ui" | "code") {
_setPreviewTab(tab);
}
const themeObj =
props.options.theme.type === "dark"
? darkTheme({
colors: props.options.theme.darkColorOverrides,
})
: lightTheme({
colors: props.options.theme.lightColorOverrides,
});
return (
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:max-h-[100vh] xl:w-[764px]">
<TabButtons
tabs={[
{
isActive: previewTab === "ui",
name: "Preview",
onClick: () => setPreviewTab("ui"),
},
{
isActive: previewTab === "code",
name: "Code",
onClick: () => setPreviewTab("code"),
},
]}
/>
<div
className={cn(
"relative flex min-h-[300px] grow justify-center rounded-lg",
previewTab !== "code" && "items-center",
)}
>
{previewTab === "ui" && props.options.integrationType !== "iframe" && (
<BridgeWidget
className="shadow-xl"
client={THIRDWEB_CLIENT}
theme={themeObj}
buy={
props.options.prefill?.buyToken
? {
chainId: props.options.prefill.buyToken.chainId,
tokenAddress: props.options.prefill.buyToken.tokenAddress,
amount: props.options.prefill.buyToken.amount,
}
: undefined
}
swap={{
persistTokenSelections: false,
prefill: {
buyToken: props.options.prefill?.buyToken
? {
chainId: props.options.prefill.buyToken.chainId,
tokenAddress: props.options.prefill.buyToken.tokenAddress,
amount: props.options.prefill.buyToken.amount,
}
: undefined,
sellToken: props.options.prefill?.sellToken
? {
chainId: props.options.prefill.sellToken.chainId,
tokenAddress:
props.options.prefill.sellToken.tokenAddress,
amount: props.options.prefill.sellToken.amount,
}
: undefined,
},
}}
currency={props.options.currency}
showThirdwebBranding={props.options.showThirdwebBranding}
key={JSON.stringify(props.options.prefill)}
/>
)}
{previewTab === "ui" && props.options.integrationType === "iframe" && (
<iframe
src={buildIframeUrl(props.options, "preview")}
height="750px"
width="100%"
title="Bridge Widget"
className="fade-in-0 duration-500 animate-in rounded-xl"
style={{
border: "0",
}}
/>
)}
{previewTab === "code" && <CodeGen options={props.options} />}
</div>
</div>
);
}
function TabButtons(props: {
tabs: Array<{
name: string;
isActive: boolean;
onClick: () => void;
}>;
}) {
return (
<div>
<div className="inline-flex justify-start gap-1 rounded-lg border bg-card p-1.5">
{props.tabs.map((tab) => (
<Button
className={cn(
"gap-2 px-4",
tab.isActive
? "bg-accent text-foreground"
: "bg-transparent text-muted-foreground",
)}
key={tab.name}
onClick={tab.onClick}
variant="ghost"
>
{tab.name}
</Button>
))}
</div>
</div>
);
}
@@ -0,0 +1,28 @@
import type {
SupportedFiatCurrency,
SwapWidgetProps,
ThemeOverrides,
} from "thirdweb/react";
export type BridgeWidgetPlaygroundOptions = {
integrationType: "script" | "component" | "iframe";
theme: {
type: "dark" | "light";
darkColorOverrides: ThemeOverrides["colors"];
lightColorOverrides: ThemeOverrides["colors"];
};
currency?: SupportedFiatCurrency;
prefill?: {
buyToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
sellToken?: {
tokenAddress?: string;
chainId: number;
amount?: string;
};
};
showThirdwebBranding?: SwapWidgetProps["showThirdwebBranding"];
};
@@ -0,0 +1,33 @@
import { BringToFrontIcon } from "lucide-react";
import { PageLayout } from "@/components/blocks/APIHeader";
import ThirdwebProvider from "@/components/thirdweb-provider";
import { createMetadata } from "@/lib/metadata";
import { BridgeWidgetPlayground } from "./components/bridge-playground";
const title = "Bridge Widget";
const description =
"Easily embed cross-chain token swap and fiat onramp UI into your app";
export const metadata = createMetadata({
description: description,
title,
image: {
icon: "payments",
title,
},
});
export default function Page() {
return (
<ThirdwebProvider>
<PageLayout
icon={BringToFrontIcon}
title={title}
description={description}
docsLink="https://portal.thirdweb.com/bridge/bridge-widget?utm_source=playground"
>
<BridgeWidgetPlayground />
</PageLayout>
</ThirdwebProvider>
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { BringToFrontIcon } from "lucide-react";
import { OverviewPage } from "@/components/blocks/OverviewPage";
import { PayIcon } from "@/icons/PayIcon";
import { bridgeFeatureCards } from "../data/pages-metadata";
export default function Page() {
@@ -8,7 +8,7 @@ export default function Page() {
title="Bridge"
description="thirdweb Bridge lets developers swap and transfer any token across any chain instantly"
featureCards={bridgeFeatureCards}
icon={PayIcon}
icon={BringToFrontIcon}
/>
);
}
@@ -3,7 +3,6 @@ import {
BotIcon,
BoxIcon,
CircleUserIcon,
CreditCardIcon,
DollarSignIcon,
GlobeIcon,
ImageIcon,
@@ -14,7 +13,7 @@ import {
RectangleHorizontalIcon,
RssIcon,
ShieldIcon,
ShoppingBagIcon,
SquareIcon,
SquircleDashedIcon,
StampIcon,
UserIcon,
@@ -118,27 +117,34 @@ export const headlessComponentsFeatureCards: FeatureCardMetadata[] = [
export const bridgeFeatureCards: FeatureCardMetadata[] = [
{
icon: ArrowLeftRightIcon,
icon: SquareIcon,
title: "Bridge Widget",
link: "/bridge/bridge-widget",
description:
"A widget for bridging tokens with cross-chain support and fiat onramp",
},
{
icon: SquareIcon,
title: "Swap Widget",
link: "/bridge/swap-widget",
description: "A widget for swapping tokens with cross-chain support",
},
{
icon: ShoppingBagIcon,
icon: SquareIcon,
title: "Buy Widget",
link: "/bridge/buy-widget",
description:
"Buy any token with ability to customize theme, amounts, and more",
"A widget to buy tokens with fiat onramp support or cross-chain swaps",
},
{
icon: CreditCardIcon,
icon: SquareIcon,
title: "Checkout Widget",
link: "/bridge/checkout-widget",
description:
"Enable crypto payments for services and get notified on each sale",
},
{
icon: ArrowLeftRightIcon,
icon: SquareIcon,
title: "Transaction Widget",
link: "/bridge/transaction-widget",
description:
+4
View File
@@ -187,6 +187,10 @@ const bridge: ShadcnSidebarLink = {
href: "/bridge",
exactMatch: true,
},
{
href: "/bridge/bridge-widget",
label: "Bridge Widget",
},
{
href: "/bridge/swap-widget",
label: "Swap Widget",
@@ -31,9 +31,8 @@ export default function Page() {
function UserTransactions() {
return (
<>
<CodeExample
code={`\
<CodeExample
code={`\
import { inAppWallet } from "thirdweb/wallets/in-app";
import { ConnectButton, useActiveWallet } from "thirdweb/react";
@@ -88,14 +87,13 @@ function App() {
</>
);
}`}
header={{
description:
"Queue, monitor, and retry transactions from your users in-app wallets. All transactions and analytics will be displayed in your developer dashboard.",
title: "Transactions from User Wallets",
}}
lang="tsx"
preview={<GatewayPreview />}
/>
</>
header={{
description:
"Queue, monitor, and retry transactions from your users in-app wallets. All transactions and analytics will be displayed in your developer dashboard.",
title: "Transactions from User Wallets",
}}
lang="tsx"
preview={<GatewayPreview />}
/>
);
}
@@ -66,6 +66,7 @@ export function X402LeftSection(props: {
const handleTokenChange = (token: TokenMetadata) => {
setSelectedToken({
address: token.address,
// biome-ignore lint/style/noNonNullAssertion: ok
chainId: selectedChain!,
});
@@ -13,7 +13,7 @@ export function FeatureCard(props: {
<props.icon className="size-4 text-muted-foreground" />
</div>
</div>
<h3 className="font-medium mb-0.5 text-lg tracking-tight">
<h3 className="font-semibold mb-0.5 text-lg tracking-tight">
<Link className="before:absolute before:inset-0" href={props.href}>
{props.title}
</Link>
@@ -0,0 +1,139 @@
"use client";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { ScrollShadow } from "@/components/ui/ScrollShadow";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export function TabButtons(props: {
tabs: {
name: React.ReactNode;
onClick: () => void;
isActive: boolean;
icon?: React.FC<{ className?: string }>;
toolTip?: string;
}[];
tabClassName?: string;
activeTabClassName?: string;
tabContainerClassName?: string;
containerClassName?: string;
shadowColor?: string;
tabIconClassName?: string;
hideBottomLine?: boolean;
bottomLineClassName?: string;
}) {
const { containerRef, lineRef, activeTabRef } =
useUnderline<HTMLButtonElement>();
return (
<div className={cn("relative", props.containerClassName)}>
{/* Bottom line */}
{!props.hideBottomLine && (
<div
className={cn(
"absolute right-0 bottom-0 left-0 h-[1px] bg-border",
props.bottomLineClassName,
)}
/>
)}
<ScrollShadow
scrollableClassName="pb-[8px] relative"
shadowColor={props.shadowColor}
>
<div
className={cn("flex", props.tabContainerClassName)}
ref={containerRef}
>
{props.tabs.map((tab, index) => {
return (
<ToolTipLabel
key={typeof tab.name === "string" ? tab.name : index}
label={tab.toolTip}
>
<Button
className={cn(
"relative inline-flex h-auto items-center gap-1.5 rounded-lg font-medium hover:bg-accent !px-3",
!tab.isActive &&
"text-muted-foreground hover:text-foreground",
props.tabClassName,
tab.isActive && props.activeTabClassName,
)}
onClick={tab.onClick}
ref={tab.isActive ? activeTabRef : undefined}
variant="ghost"
>
{tab.icon && (
<tab.icon
className={cn("size-6", props.tabIconClassName)}
/>
)}
{tab.name}
</Button>
</ToolTipLabel>
);
})}
</div>
{/* Active line */}
<div
className="fade-in-0 absolute bottom-0 left-0 h-[2px] animate-in rounded-lg bg-foreground"
ref={lineRef}
/>
</ScrollShadow>
</div>
);
}
function useUnderline<El extends HTMLElement>() {
const containerRef = useRef<HTMLDivElement>(null);
const lineRef = useRef<HTMLDivElement>(null);
const [activeTabEl, setActiveTabEl] = useState<El | null>(null);
const activeTabRef = useCallback((el: El | null) => {
setActiveTabEl(el);
}, []);
useLayoutEffect(() => {
function update() {
if (activeTabEl && containerRef.current && lineRef.current) {
const containerRect = containerRef.current.getBoundingClientRect();
const lineEl = lineRef.current;
const rect = activeTabEl.getBoundingClientRect();
const containerPaddingLeft =
containerRect.left - containerRef.current.offsetLeft;
lineEl.style.width = `${rect.width}px`;
lineEl.style.transform = `translateX(${
rect.left - containerPaddingLeft
}px)`;
setTimeout(() => {
lineEl.style.transition = "transform 0.3s, width 0.3s";
}, 0);
} else if (lineRef.current) {
lineRef.current.style.width = "0px";
}
}
update();
let resizeObserver: ResizeObserver | undefined;
if (containerRef.current) {
resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
update();
}, 100);
});
resizeObserver.observe(containerRef.current);
}
// add event listener for resize
window.addEventListener("resize", update);
return () => {
window.removeEventListener("resize", update);
resizeObserver?.disconnect();
};
}, [activeTabEl]);
return { activeTabRef, containerRef, lineRef };
}
+16 -1
View File
@@ -30,7 +30,22 @@ async function fetchBridgeSupportedChainsFromApi() {
throw new Error(json.error.message);
}
return json.data as BridgeChain[];
const data = json.data as BridgeChain[];
return data.sort((a, b) => {
const aStartsWithNumber = a.name[0]?.match(/^\d/);
const bStartsWithNumber = b.name[0]?.match(/^\d/);
if (aStartsWithNumber && !bStartsWithNumber) {
return 1;
}
if (!aStartsWithNumber && bStartsWithNumber) {
return -1;
}
return a.name.localeCompare(b.name);
});
}
export function useBridgeSupportedChains() {
-21
View File
@@ -1,21 +0,0 @@
export function PayIcon(props: { className?: string }) {
return (
<svg
className={props.className}
fill="none"
height="24"
role="presentation"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 10H2M2 8.2L2 15.8C2 16.9201 2 17.4802 2.21799 17.908C2.40973 18.2843 2.71569 18.5903 3.09202 18.782C3.51984 19 4.07989 19 5.2 19L18.8 19C19.9201 19 20.4802 19 20.908 18.782C21.2843 18.5903 21.5903 18.2843 21.782 17.908C22 17.4802 22 16.9201 22 15.8V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.7157 21.2843 5.40974 20.908 5.21799C20.4802 5 19.9201 5 18.8 5L5.2 5C4.0799 5 3.51984 5 3.09202 5.21799C2.7157 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.07989 2 8.2Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
}
@@ -55,14 +55,14 @@ You can set the light theme by passing the `theme=light` query parameter.
<IframeCodePreview src="https://thirdweb.com/bridge/widget?theme=light" />
### Change default token selections
### Token selections
By default no tokens are selected in the widget UI.
You can change the default token selections by using query parameters mentioned below:
* `inputCurrency`, `inputChain` to set sell token.
* `outputCurrency`, `outputChain` to set buy token.
* `inputCurrency`, `inputChain`, `inputCurrencyAmount` to set sell token and amount.
* `outputCurrency`, `outputChain`, `outputCurrencyAmount` to set buy token and amount.
Chain parameter should be the chain id (eg. 1 for Ethereum Mainnet, 137 for Polygon Mainnet, 8453 for Base Mainnet, etc.)
and currency parameter should be the token address. You can use [thirdweb chainlist](https://thirdweb.com/chainlist?service=pay) to find the chain id for a chain.
@@ -100,6 +100,27 @@ Set USDC on Base as the default sell token and USDC on Polygon as the default bu
</Details>
### Token amounts
You can set default token amounts using the `inputCurrencyAmount` and `outputCurrencyAmount` query parameters.
<Details summary="Set default sell token amount">
Set 100 USDC on Base as the default sell token with amount.
<IframeCodePreview src="https://thirdweb.com/bridge/widget?inputCurrency=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&inputChain=8453&inputCurrencyAmount=100" />
</Details>
<Details summary="Set default buy token amount">
Set 50 USDC on Polygon as the default buy token with amount.
<IframeCodePreview src="https://thirdweb.com/bridge/widget?outputCurrency=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&outputChain=137&outputCurrencyAmount=50" />
</Details>
### Persist Token Selections
By default, the widget saves the last used token selections to localStorage so that if the user revisits the widget later, last used tokens are selected by default. To disable this feature, you can set the `persistTokenSelections` query parameter to `false`.
@@ -151,3 +172,10 @@ Show fiat values in Japanese Yen (JPY) in the widget.
<IframeCodePreview src="https://thirdweb.com/bridge/widget?currency=JPY" />
### thirdweb branding
By default, the widget displays thirdweb branding at the bottom. You can hide this by setting the `showThirdwebBranding` query parameter to `false`.
<IframeCodePreview src="https://thirdweb.com/bridge/widget?showThirdwebBranding=false" />
@@ -1,9 +1,13 @@
import { useEffect, useRef, useState } from "react";
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
import type { Chain } from "../../../../../chains/types.js";
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 type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import type { SmartWalletOptions } from "../../../../../wallets/smart/types.js";
import type { AppMetadata } from "../../../../../wallets/types.js";
import {
CustomThemeProvider,
useCustomTheme,
@@ -14,6 +18,8 @@ import {
spacing,
type Theme,
} from "../../../../core/design-system/index.js";
import type { SiweAuthOptions } from "../../../../core/hooks/auth/useSiweAuth.js";
import type { ConnectButton_connectModalOptions } from "../../../../core/hooks/connection/ConnectButtonProps.js";
import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js";
import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js";
import { Container } from "../../components/basic.js";
@@ -26,6 +32,8 @@ import type { SwapPreparedQuote } from "../swap-widget/types.js";
* Props for the `BridgeWidget` component.
*/
export type BridgeWidgetProps = {
className?: string;
style?: React.CSSProperties;
/**
* 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
@@ -190,6 +198,117 @@ export type BridgeWidgetProps = {
/** Arbitrary data to be included in returned status and webhook events. */
purchaseData?: PurchaseData;
};
connectOptions?: {
/**
* Configurations for the `ConnectButton`'s Modal that is shown for connecting a wallet
* Refer to the [`ConnectButton_connectModalOptions`](https://portal.thirdweb.com/references/typescript/v5/ConnectButton_connectModalOptions) type for more details
*/
connectModal?: ConnectButton_connectModalOptions;
/**
* Configure options for WalletConnect
*
* By default WalletConnect uses the thirdweb's default project id.
* Setting your own project id is recommended.
*
* You can create a project id by signing up on [walletconnect.com](https://walletconnect.com/)
*/
walletConnect?: {
projectId?: string;
};
/**
* Enable Account abstraction for all wallets. This will connect to the users's smart account based on the connected personal wallet and the given options.
*
* This allows to sponsor gas fees for your user's transaction using the thirdweb account abstraction infrastructure.
*
*/
accountAbstraction?: SmartWalletOptions;
/**
* Array of wallets to show in Connect Modal. If not provided, default wallets will be used.
*/
wallets?: Wallet[];
/**
* When the user has connected their wallet to your site, this configuration determines whether or not you want to automatically connect to the last connected wallet when user visits your site again in the future.
*
* By default it is set to `{ timeout: 15000 }` meaning that autoConnect is enabled and if the autoConnection does not succeed within 15 seconds, it will be cancelled.
*
* If you want to disable autoConnect, set this prop to `false`.
*
* If you want to customize the timeout, you can assign an object with a `timeout` key to this prop.
*/
autoConnect?:
| {
timeout: number;
}
| boolean;
/**
* Metadata of the app that will be passed to connected wallet. Setting this is highly recommended.
*/
appMetadata?: AppMetadata;
/**
* The [`Chain`](https://portal.thirdweb.com/references/typescript/v5/Chain) object of the blockchain you want the wallet to connect to
*
* If a `chain` is not specified, Wallet will be connected to whatever is the default set in the wallet.
*
* If a `chain` is specified, Wallet will be prompted to switch to given chain after connection if it is not already connected to it.
* This ensures that the wallet is connected to the correct blockchain before interacting with your app.
*
* The `ConnectButton` also shows a "Switch Network" button until the wallet is connected to the specified chain. Clicking on the "Switch Network" button triggers the wallet to switch to the specified chain.
*
* You can create a `Chain` object using the [`defineChain`](https://portal.thirdweb.com/references/typescript/v5/defineChain) function.
* At minimum, you need to pass the `id` of the blockchain to `defineChain` function to create a `Chain` object.
* ```
*/
chain?: Chain;
/**
* Array of chains that your app supports.
*
* This is only relevant if your app is a multi-chain app and works across multiple blockchains.
* If your app only works on a single blockchain, you should only specify the `chain` prop.
*
* Given list of chains will used in various ways:
* - They will be displayed in the network selector in the `ConnectButton`'s details modal post connection
* - They will be sent to wallet at the time of connection if the wallet supports requesting multiple chains ( example: WalletConnect ) so that users can switch between the chains post connection easily
*
* You can create a `Chain` object using the [`defineChain`](https://portal.thirdweb.com/references/typescript/v5/defineChain) function.
* At minimum, you need to pass the `id` of the blockchain to `defineChain` function to create a `Chain` object.
*
* ```tsx
* import { defineChain } from "thirdweb/react";
*
* const polygon = defineChain({
* id: 137,
* });
* ```
*/
chains?: Chain[];
/**
* Wallets to show as recommended in the `ConnectButton`'s Modal
*/
recommendedWallets?: Wallet[];
/**
* By default, ConnectButton modal shows a "All Wallets" button that shows a list of 500+ wallets.
*
* You can disable this button by setting `showAllWallets` prop to `false`
*/
showAllWallets?: boolean;
/**
* Enable SIWE (Sign in with Ethererum) by passing an object of type `SiweAuthOptions` to
* enforce the users to sign a message after connecting their wallet to authenticate themselves.
*
* Refer to the [`SiweAuthOptions`](https://portal.thirdweb.com/references/typescript/v5/SiweAuthOptions) for more details
*/
auth?: SiweAuthOptions;
};
};
/**
@@ -258,8 +377,10 @@ export function BridgeWidget(props: BridgeWidgetProps) {
<CustomThemeProvider theme={props.theme}>
<EmbedContainer
modalSize="compact"
className={props.className}
style={{
borderRadius: radius.xl,
...props.style,
}}
>
<Container
@@ -294,6 +415,7 @@ export function BridgeWidget(props: BridgeWidgetProps) {
onCancel={props.swap?.onCancel}
onDisconnect={props.swap?.onDisconnect}
persistTokenSelections={props.swap?.persistTokenSelections}
connectOptions={props.connectOptions}
style={{
border: "none",
...props.swap?.style,
@@ -321,6 +443,7 @@ export function BridgeWidget(props: BridgeWidgetProps) {
presetOptions={props.buy?.presetOptions}
purchaseData={props.buy?.purchaseData}
paymentMethods={["card"]}
connectOptions={props.connectOptions}
style={{
border: "none",
}}
@@ -345,8 +468,12 @@ function TabButton(props: {
borderRadius: radius.full,
fontSize: fontSize.sm,
fontWeight: 500,
paddingInline: spacing["md+"],
paddingBlock: spacing.sm,
paddingInline: spacing.md,
paddingBlock: 0,
height: "36px",
color: props.isActive
? theme.colors.primaryText
: theme.colors.secondaryText,
border: `1px solid ${
props.isActive ? theme.colors.secondaryText : theme.colors.borderColor
}`,
@@ -17,7 +17,7 @@ import {
const three3dModelLink =
"https://i2.seadn.io/matic/0x2953399124f0cbb46d2cbacd8a89cf0599974963/bd1801876c5cf1302484e225c72959/49bd1801876c5cf1302484e225c72959.glb";
const imageLink =
"https://i.seadn.io/gae/r_b9GB0iYA39ichUlKdFLeG4UliK7YXi9SsM0Xdvm6pNDChYbN5E7Fxop1MdJCbmNvSlbER73YiA9WY1JbhEfkuIktoHfN9UlEZy4A?auto=format&dpr=1&w=1000";
"https://i2.seadn.io/ethereum/0xbd3531da5cf5857e7cfaa92426877b022e612cf8/750c4805cd12ea19c8c3909677e61988.png?w=350";
describe("MediaRenderer", () => {
it("should render nothing if no src provided", () => {
@@ -32,7 +32,7 @@ describe("MediaRenderer", () => {
}, 1000);
});
it("should render a plain image", async () => {
it.skip("should render a plain image", async () => {
render(<MediaRenderer client={TEST_CLIENT} src={imageLink} />);
await waitFor(() => {
expect(screen.getByRole("img")).toBeInTheDocument();
@@ -19,16 +19,6 @@ export function BasicUsage() {
);
}
export function LightTheme() {
return (
<Variant
clientId={storyClient.clientId}
theme="light"
buy={{ chainId: 8453, amount: "0.1" }}
/>
);
}
export function CurrencySet() {
return (
<Variant
@@ -52,20 +42,29 @@ export function NoThirdwebBranding() {
export function CustomTheme() {
return (
<Variant
clientId={storyClient.clientId}
buy={{ chainId: 8453, amount: "0.1" }}
theme={{
type: "light",
colors: {
modalBg: "#FFFFF0",
tertiaryBg: "#DBE4C9",
borderColor: "#8AA624",
secondaryText: "#3E3F29",
accentText: "#E43636",
},
<div
style={{
display: "flex",
flexDirection: "column",
gap: "40px",
alignItems: "center",
}}
/>
>
<BridgeWidgetScript
clientId={storyClient.clientId}
buy={{ chainId: 8453, amount: "0.1" }}
theme={{
type: "light",
colors: {
modalBg: "#FFFFF0",
tertiaryBg: "#DBE4C9",
borderColor: "#8AA624",
secondaryText: "#3E3F29",
accentText: "#E43636",
},
}}
/>
</div>
);
}