[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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+22
-23
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user