"use client"; import { useEffect, useRef, useState } from "react"; import { trackPayEvent } from "../../../../../analytics/track/pay.js"; import type { Buy, Sell } from "../../../../../bridge/index.js"; import type { TokenWithPrices } from "../../../../../bridge/types/Token.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js"; import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; import { getAddress } from "../../../../../utils/address.js"; import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; import { CustomThemeProvider } from "../../../../core/design-system/CustomThemeProvider.js"; import type { Theme } from "../../../../core/design-system/index.js"; import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js"; import type { CompletedStatusResult } from "../../../../core/hooks/useStepExecutor.js"; import { webWindowAdapter } from "../../../adapters/WindowAdapter.js"; import { EmbedContainer } from "../../ConnectWallet/Modal/ConnectEmbed.js"; import { DynamicHeight } from "../../components/DynamicHeight.js"; import { ErrorBanner } from "../ErrorBanner.js"; import { SuccessScreen } from "../payment-success/SuccessScreen.js"; import { StepRunner } from "../StepRunner.js"; import { useActiveWalletInfo } from "./hooks.js"; import { getLastUsedTokens, setLastUsedTokens } from "./storage.js"; import { SwapUI } from "./swap-ui.js"; import type { SwapPreparedQuote, SwapWidgetConnectOptions, TokenSelection, } from "./types.js"; import { useBridgeChains } from "./use-bridge-chains.js"; export type SwapWidgetProps = { /** * A client is the entry point to the thirdweb SDK. * It is required for all other actions. * You can create a client using the `createThirdwebClient` function. Refer to the [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation for more information. * * You must provide a `clientId` or `secretKey` in order to initialize a client. Pass `clientId` if you want for client-side usage and `secretKey` for server-side usage. * * ```tsx * import { createThirdwebClient } from "thirdweb"; * * const client = createThirdwebClient({ * clientId: "", * }) * ``` */ client: ThirdwebClient; /** * Prefill Buy and/or Sell tokens for the swap widget. If `tokenAddress` is not provided, the native token will be used * * @example * * ### Set an ERC20 token as the buy token * ```ts * * ``` * * ### Set a native token as the sell token * * ```ts * * ``` * * ### Set 0.1 Base USDC as the buy token * ```ts * * ``` * * ### Set Base USDC as the buy token and Base native token as the sell token * ```ts * * ``` */ prefill?: { buyToken?: { tokenAddress?: string; chainId: number; amount?: string; }; sellToken?: { tokenAddress?: string; chainId: number; amount?: string; }; }; /** * Set the theme for the `SwapWidget` component. By default it is set to `"dark"` * * theme can be set to either `"dark"`, `"light"` or a custom theme object. * You can also import [`lightTheme`](https://portal.thirdweb.com/references/typescript/v5/lightTheme) * or [`darkTheme`](https://portal.thirdweb.com/references/typescript/v5/darkTheme) * functions from `thirdweb/react` to use the default themes as base and overrides parts of it. * @example * ```ts * import { lightTheme } from "thirdweb/react"; * * const customTheme = lightTheme({ * colors: { * modalBg: 'red' * } * }) * * function Example() { * return * } * ``` */ theme?: "light" | "dark" | Theme; /** * The currency to use for the payment. * @default "USD" */ currency?: SupportedFiatCurrency; connectOptions?: SwapWidgetConnectOptions; /** * Whether to show thirdweb branding in the widget. * @default true */ showThirdwebBranding?: boolean; /** * Callback to be called when the swap is successful. */ onSuccess?: (data: { quote: SwapPreparedQuote; statuses: CompletedStatusResult[]; }) => void; /** * Callback to be called when user encounters an error when swapping. */ onError?: (error: Error, quote: SwapPreparedQuote) => void; /** * Callback to be called when the user cancels the purchase. */ onCancel?: (quote: SwapPreparedQuote) => void; style?: React.CSSProperties; className?: string; /** * Whether to persist the token selections to localStorage so that if the user revisits the widget, the last used tokens are pre-selected. * The last used tokens do not override the tokens specified in the `prefill` prop * * @default true */ persistTokenSelections?: boolean; /** * Called when the user disconnects the active wallet */ onDisconnect?: () => void; /** * The wallet that should be pre-selected in the SwapWidget UI. */ activeWallet?: Wallet; }; /** * A widget for swapping tokens with cross-chain support * * @param props - Props of type [`SwapWidgetProps`](https://portal.thirdweb.com/references/typescript/v5/SwapWidgetProps) to configure the SwapWidget component. * * @example * ### Basic usage * * By default, no tokens are selected in the widget UI. * * You can set specific tokens to buy or sell by default by passing the `prefill` prop. User can change these selections in the widget UI. * * ```tsx * * ``` * * ### Set an ERC20 token to Buy by default * * ```tsx * * ``` * * ### Set a native token to Sell by default * * By not specifying a `tokenAddress`, the native token will be used. * * ```tsx * * ``` * * ### Set amount and token to Buy by default * * ```tsx * * ``` * * ### Set both buy and sell tokens by default * * ```tsx * * ``` * * @bridge */ export function SwapWidget(props: SwapWidgetProps) { const hasFiredRenderEvent = useRef(false); useEffect(() => { if (hasFiredRenderEvent.current) return; hasFiredRenderEvent.current = true; trackPayEvent({ client: props.client, event: "ub:ui:swap_widget:render", }); }, [props.client]); return ( ); } /** * @internal */ export function SwapWidgetContainer(props: { theme: SwapWidgetProps["theme"]; className: string | undefined; style?: React.CSSProperties | undefined; children: React.ReactNode; }) { return ( {props.children} ); } type SelectionInfo = { preparedQuote: SwapPreparedQuote; request: BridgePrepareRequest; quote: Buy.quote.Result | Sell.quote.Result; buyToken: TokenWithPrices; sellToken: TokenWithPrices; sellTokenBalance: bigint; mode: "buy" | "sell"; }; type Join = T & U; type SwapWidgetScreen = | { id: "1:swap-ui" } | Join<{ id: "2:preview" }, SelectionInfo> | Join<{ id: "3:execute" }, SelectionInfo> | Join< { id: "4:success"; completedStatuses: CompletedStatusResult[]; }, SelectionInfo > | { id: "error"; error: Error; preparedQuote: SwapPreparedQuote }; function SwapWidgetContent( props: SwapWidgetProps & { currency: SupportedFiatCurrency; }, ) { const [screen, setScreen] = useState({ id: "1:swap-ui" }); const activeWalletInfo = useActiveWalletInfo(props.activeWallet); const isPersistEnabled = props.persistTokenSelections !== false; const [amountSelection, setAmountSelection] = useState<{ type: "buy" | "sell"; amount: string; }>(() => { if (props.prefill?.buyToken?.amount) { return { type: "buy", amount: props.prefill.buyToken.amount, }; } if (props.prefill?.sellToken?.amount) { return { type: "sell", amount: props.prefill.sellToken.amount, }; } return { type: "buy", amount: "", }; }); const [buyToken, setBuyToken] = useState(() => { return getInitialTokens(props.prefill, isPersistEnabled).buyToken; }); const [sellToken, setSellToken] = useState(() => { return getInitialTokens(props.prefill, isPersistEnabled).sellToken; }); // persist selections to localStorage whenever they change useEffect(() => { if (isPersistEnabled) { setLastUsedTokens({ buyToken, sellToken }); } }, [buyToken, sellToken, isPersistEnabled]); // preload requests useBridgeChains({ client: props.client }); // if wallet suddenly disconnects, show screen 1 if (screen.id === "1:swap-ui" || !activeWalletInfo) { return ( { setScreen({ id: "3:execute", buyToken: data.buyToken, sellToken: data.sellToken, sellTokenBalance: data.sellTokenBalance, mode: data.mode, preparedQuote: data.result, request: data.request, quote: data.result, }); }} /> ); } if (screen.id === "3:execute") { return ( { setScreen({ ...screen, id: "1:swap-ui", }); }} onCancel={() => props.onCancel?.(screen.preparedQuote)} onComplete={(completedStatuses) => { props.onSuccess?.({ quote: screen.preparedQuote, statuses: completedStatuses, }); setScreen({ ...screen, id: "4:success", completedStatuses, }); }} request={screen.request} wallet={activeWalletInfo.activeWallet} windowAdapter={webWindowAdapter} /> ); } if (screen.id === "4:success") { return ( { setScreen({ id: "1:swap-ui" }); // clear amounts setAmountSelection({ type: "buy", amount: "", }); }} preparedQuote={screen.preparedQuote} showContinueWithTx={false} windowAdapter={webWindowAdapter} hasPaymentId={false} // TODO Question: Do we need to expose this as prop? /> ); } if (screen.id === "error") { return ( { setScreen({ id: "1:swap-ui" }); props.onCancel?.(screen.preparedQuote); }} onRetry={() => { setScreen({ id: "1:swap-ui" }); }} /> ); } return null; } function getInitialTokens( prefill: SwapWidgetProps["prefill"], isPersistEnabled: boolean, ): { buyToken: TokenSelection | undefined; sellToken: TokenSelection | undefined; } { const lastUsedTokens = isPersistEnabled ? getLastUsedTokens() : undefined; const buyToken = prefill?.buyToken ? { tokenAddress: prefill.buyToken.tokenAddress || getAddress(NATIVE_TOKEN_ADDRESS), chainId: prefill.buyToken.chainId, } : lastUsedTokens?.buyToken; const sellToken = prefill?.sellToken ? { tokenAddress: prefill.sellToken.tokenAddress || getAddress(NATIVE_TOKEN_ADDRESS), chainId: prefill.sellToken.chainId, } : lastUsedTokens?.sellToken; // if both tokens are same if ( buyToken && sellToken && buyToken.tokenAddress?.toLowerCase() === sellToken.tokenAddress?.toLowerCase() && buyToken.chainId === sellToken.chainId ) { // if sell token prefill is specified, ignore buy token if (prefill?.sellToken) { return { buyToken: undefined, sellToken: sellToken, }; } // if buy token prefill is specified, ignore sell token if (prefill?.buyToken) { return { buyToken: buyToken, sellToken: undefined, }; } // if none of the two are specified via prefill, keep buy token return { buyToken: buyToken, sellToken: undefined, }; } return { buyToken: buyToken, sellToken: sellToken, }; }