+ {props.widget === "buy" && (
+
+
+
+ Receive the tokens in a different wallet address
+
+
{
+ setOptions((v) => ({
+ ...v,
+ payOptions: {
+ ...v.payOptions,
+ receiverAddress: e.target.value as Address,
+ },
+ }));
+ }}
+ placeholder="0x..."
+ value={payOptions.receiverAddress || ""}
+ />
+
+ )}
+
{/* Payment Methods */}
@@ -206,6 +231,7 @@ export function LeftSection(props: {
/>
+
-
void;
};
/**
@@ -326,31 +319,12 @@ export type BuyWidgetProps = {
* @bridge
*/
export function BuyWidget(props: BuyWidgetProps) {
- return (
-
-
-
- );
-}
-
-function BridgeWidgetContentWrapper(props: BuyWidgetProps) {
- const localQuery = useConnectLocale(props.locale || "en_US");
- const tokenQuery = useTokenQuery({
- tokenAddress: props.tokenAddress,
- chainId: props.chain.id,
- client: props.client,
- });
-
useQuery({
queryFn: () => {
trackPayEvent({
client: props.client,
event: "ub:ui:buy_widget:render",
- toChainId: props.chain.id,
+ toChainId: props.chain?.id,
toToken: props.tokenAddress,
});
return true;
@@ -372,33 +346,15 @@ function BridgeWidgetContentWrapper(props: BuyWidgetProps) {
return props.connectOptions;
}, [props.connectOptions, props.showThirdwebBranding]);
- if (tokenQuery.isPending || !localQuery.data) {
- return (
-
-
-
- );
- } else if (tokenQuery.data?.type === "unsupported_token") {
- return (
-
- );
- } else if (tokenQuery.data?.type === "success") {
- return (
+ return (
+
- );
- } else if (tokenQuery.error) {
- return (
- {
- tokenQuery.refetch();
- }}
- onCancel={() => {
- props.onCancel?.(undefined);
- }}
- />
- );
- }
-
- return null;
+
+ );
}
type BuyWidgetScreen =
@@ -475,13 +416,15 @@ type BuyWidgetScreen =
function BridgeWidgetContent(
props: RequiredParams<
BuyWidgetProps,
- "currency" | "presetOptions" | "showThirdwebBranding" | "paymentMethods"
- > & {
- connectLocale: ConnectLocale;
- destinationToken: TokenWithPrices;
- },
+ | "currency"
+ | "presetOptions"
+ | "showThirdwebBranding"
+ | "paymentMethods"
+ | "theme"
+ >,
) {
const [screen, setScreen] = useState
({ id: "1:buy-ui" });
+ const activeWalletInfo = useActiveWalletInfo();
const handleError = useCallback(
(error: Error, quote: BridgePrepareResult | undefined) => {
@@ -511,9 +454,11 @@ function BridgeWidgetContent(
[props.onCancel],
);
- if (screen.id === "1:buy-ui") {
+ if (screen.id === "1:buy-ui" || !activeWalletInfo) {
return (
{
@@ -534,8 +479,11 @@ function BridgeWidgetContent(
}}
buttonLabel={props.buttonLabel}
currency={props.currency}
- initialAmount={props.amount}
- destinationToken={props.destinationToken}
+ initialSelection={{
+ tokenAddress: props.tokenAddress,
+ chainId: props.chain?.id,
+ amount: props.amount,
+ }}
/>
);
}
@@ -545,7 +493,7 @@ function BridgeWidgetContent(
void) | undefined;
+
/**
* The metadata to display in the widget.
*/
@@ -92,297 +116,598 @@ type FundWalletProps = {
};
};
-export function FundWallet({
- client,
- receiverAddress,
- onContinue,
- presetOptions,
- connectOptions,
- showThirdwebBranding,
- initialAmount,
- destinationToken,
- currency,
- buttonLabel,
- metadata,
-}: FundWalletProps) {
- const [amount, setAmount] = useState(initialAmount ?? "");
- const theme = useCustomTheme();
- const account = useActiveAccount();
- const receiver = receiverAddress ?? account?.address;
+type SelectedToken =
+ | {
+ chainId: number;
+ tokenAddress: string;
+ }
+ | undefined;
- const handleAmountChange = (inputValue: string) => {
- let processedValue = inputValue;
+type AmountSelection =
+ | {
+ type: "usd";
+ value: string;
+ }
+ | {
+ type: "token";
+ value: string;
+ };
- // Replace comma with period if it exists
- processedValue = processedValue.replace(",", ".");
+export function FundWallet(props: FundWalletProps) {
+ const [amountSelection, setAmountSelection] = useState({
+ type: "token",
+ value: props.initialSelection.amount ?? "",
+ });
+ const theme = useCustomTheme();
+ const activeWalletInfo = useActiveWalletInfo();
+ const receiver =
+ props.receiverAddress ?? activeWalletInfo?.activeAccount?.address;
- if (processedValue.startsWith(".")) {
- processedValue = `0${processedValue}`;
- }
+ const [detailsModalOpen, setDetailsModalOpen] = useState(false);
+ const [isTokenSelectionOpen, setIsTokenSelectionOpen] = useState(false);
- const numValue = Number(processedValue);
- if (Number.isNaN(numValue)) {
- return;
- }
+ const isReceiverDifferentFromActiveWallet =
+ props.receiverAddress &&
+ isAddress(props.receiverAddress) &&
+ (activeWalletInfo?.activeAccount?.address
+ ? checksumAddress(props.receiverAddress) !==
+ checksumAddress(activeWalletInfo?.activeAccount?.address)
+ : true);
- if (processedValue.startsWith("0") && !processedValue.startsWith("0.")) {
- setAmount(processedValue.slice(1));
- } else {
- setAmount(processedValue);
+ const [selectedToken, setSelectedToken] = useState(() => {
+ if (!props.initialSelection.chainId) {
+ return undefined;
}
- };
- const getAmountFontSize = () => {
- const length = amount.length;
- if (length > 12) return fontSize.md;
- if (length > 8) return fontSize.lg;
- return fontSize.xl;
- };
+ return {
+ chainId: props.initialSelection.chainId,
+ tokenAddress: props.initialSelection.tokenAddress || NATIVE_TOKEN_ADDRESS,
+ };
+ });
- const isValidAmount = amount && Number(amount) > 0;
+ const tokenQuery = useTokenQuery({
+ tokenAddress: selectedToken?.tokenAddress,
+ chainId: selectedToken?.chainId,
+ client: props.client,
+ });
- const inputRef = useRef(null);
+ const destinationToken =
+ tokenQuery.data?.type === "success" ? tokenQuery.data.token : undefined;
- const focusInput = () => {
- inputRef.current?.focus();
- };
+ const tokenBalanceQuery = useTokenBalance({
+ chainId: selectedToken?.chainId,
+ tokenAddress: selectedToken?.tokenAddress,
+ client: props.client,
+ walletAddress: activeWalletInfo?.activeAccount?.address,
+ });
- const handleQuickAmount = (usdAmount: number) => {
- const price = destinationToken.prices[currency || "USD"] || 0;
- if (price === 0) {
- return;
- }
- // Convert USD amount to token amount using token price
- const tokenAmount = usdAmount / price;
- // Format to reasonable decimal places (up to 6 decimals, remove trailing zeros)
- const formattedAmount = numberToPlainString(
- Number.parseFloat(tokenAmount.toFixed(6)),
- );
- setAmount(formattedAmount);
- };
+ const actionLabel = isReceiverDifferentFromActiveWallet ? "Pay" : "Buy";
+ const isMobile = useIsMobile();
return (
+ {detailsModalOpen && (
+ {
+ setDetailsModalOpen(false);
+ }}
+ onDisconnect={() => {
+ props.onDisconnect?.();
+ }}
+ chains={[]}
+ connectOptions={props.connectOptions}
+ />
+ )}
+
+ setIsTokenSelectionOpen(v)}
+ >
+ setIsTokenSelectionOpen(false)}
+ client={props.client}
+ selectedToken={selectedToken}
+ setSelectedToken={(token) => {
+ setSelectedToken(token);
+ setIsTokenSelectionOpen(false);
+ }}
+ />
+
+
{/* Token Info */}
-
-
- {/* Amount Input */}
-
- ) => {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- focusInput();
+
-
- {
- handleAmountChange(e.target.value);
- }}
- onClick={(e) => {
- // put cursor at the end of the input
- if (amount === "") {
- e.currentTarget.setSelectionRange(
- e.currentTarget.value.length,
- e.currentTarget.value.length,
- );
- }
- }}
- pattern="^[0-9]*[.,]?[0-9]*$"
- placeholder="0"
- ref={inputRef}
- style={{
- border: "none",
- boxShadow: "none",
- fontSize: getAmountFontSize(),
- fontWeight: 600,
- padding: "0",
- textAlign: "right",
- }}
- type="text"
- value={amount || "0"}
- variant="transparent"
- />
-
-
-
- {/* Fiat Value */}
-
-
- ≈{" "}
- {formatCurrencyAmount(
- currency || "USD",
- Number(amount) *
- (destinationToken.prices[currency || "USD"] || 0),
- )}
-
-
-
-
+ : undefined
+ }
+ balance={{
+ data: tokenBalanceQuery.data?.value,
+ isFetching: tokenBalanceQuery.isFetching,
+ }}
+ client={props.client}
+ isConnected={!!activeWalletInfo}
+ onSelectToken={() => {
+ setIsTokenSelectionOpen(true);
+ }}
+ onWalletClick={() => {
+ setDetailsModalOpen(true);
+ }}
+ currency={props.currency}
+ />
- {/* Quick Amount Buttons */}
- {presetOptions && (
+ {receiver && isReceiverDifferentFromActiveWallet && (
<>
-
-
- {presetOptions?.map((amount) => (
-
- ))}
-
+
+
>
)}
-
-
-
-
- {receiver ? (
-
- ) : (
- <>
-
-
- No Wallet Connected
-
- >
- )}
-
-
+
{/* Continue Button */}
- {receiver ? (
+ {activeWalletInfo ? (
) : (
)}
- {showThirdwebBranding ? (
+ {props.showThirdwebBranding ? (
- ) : null}
+ ) : (
+
+ )}
);
}
+
+function getAmounts(
+ amountSelection: AmountSelection,
+ fiatPricePerToken: number | undefined,
+) {
+ const fiatValue =
+ amountSelection.type === "usd"
+ ? amountSelection.value
+ : fiatPricePerToken
+ ? fiatPricePerToken * Number(amountSelection.value)
+ : undefined;
+
+ const tokenValue =
+ amountSelection.type === "token"
+ ? amountSelection.value
+ : fiatPricePerToken
+ ? Number(amountSelection.value) / fiatPricePerToken
+ : undefined;
+
+ return {
+ fiatValue,
+ tokenValue,
+ };
+}
+
+function TokenSection(props: {
+ amountSelection: AmountSelection;
+ setAmount: (amountSelection: AmountSelection) => void;
+ activeWalletInfo: ActiveWalletInfo | undefined;
+ selectedToken:
+ | {
+ data: TokenWithPrices | undefined;
+ isFetching: boolean;
+ }
+ | undefined;
+ currency: SupportedFiatCurrency;
+ onSelectToken: () => void;
+ client: ThirdwebClient;
+ title: string;
+ isConnected: boolean;
+ balance: {
+ data: bigint | undefined;
+ isFetching: boolean;
+ };
+ onWalletClick: () => void;
+ presetOptions: [number, number, number];
+}) {
+ const theme = useCustomTheme();
+ const chainQuery = useBridgeChains(props.client);
+ const chain = chainQuery.data?.find(
+ (chain) => chain.chainId === props.selectedToken?.data?.chainId,
+ );
+
+ const fiatPricePerToken = props.selectedToken?.data?.prices[props.currency];
+
+ const { fiatValue, tokenValue } = getAmounts(
+ props.amountSelection,
+ fiatPricePerToken,
+ );
+
+ return (
+
+
+
+ {props.title}
+
+
+ {props.activeWalletInfo && (
+
+ )}
+
+ }
+ >
+ {/* select token */}
+
+
+
+ {/* token value input */}
+ {
+ props.setAmount({
+ type: "token",
+ value,
+ });
+ }}
+ style={{
+ border: "none",
+ boxShadow: "none",
+ fontSize: fontSize.xl,
+ fontWeight: 500,
+ paddingInline: 0,
+ paddingBlock: 0,
+ letterSpacing: "-0.025em",
+ }}
+ />
+
+
+
+ {/* fiat value input */}
+
+
+ {getFiatSymbol(props.currency)}
+
+
+ {props.selectedToken?.isFetching ? (
+
+ ) : (
+ {
+ props.setAmount({
+ type: "usd",
+ value,
+ });
+ }}
+ style={{
+ border: "none",
+ boxShadow: "none",
+ fontSize: fontSize.md,
+ fontWeight: 400,
+ color: theme.colors.secondaryText,
+ paddingInline: 0,
+ height: "20px",
+ paddingBlock: 0,
+ }}
+ />
+ )}
+
+
+
+
+ {/* suggested amounts */}
+
+ {props.presetOptions.map((amount) => (
+
+ ))}
+
+
+
+ {/* balance */}
+ {props.isConnected && props.selectedToken && (
+
+
+
+ Current Balance
+
+ {props.balance.data === undefined ||
+ props.selectedToken.data === undefined ? (
+
+ ) : (
+
+ {formatTokenAmount(
+ props.balance.data,
+ props.selectedToken.data.decimals,
+ 5,
+ )}{" "}
+ {props.selectedToken.data.symbol}
+
+ )}
+
+
+ )}
+
+ );
+}
+
+function ReceiverWalletSection(props: {
+ address: string;
+ client: ThirdwebClient;
+}) {
+ const ensNameQuery = useEnsName({
+ address: props.address,
+ client: props.client,
+ });
+
+ return (
+
+ To
+
+ }
+ >
+
+
+
+ {ensNameQuery.data || shortenAddress(props.address)}
+
+
+
+
+ );
+}
+
+function SectionContainer(props: {
+ children: React.ReactNode;
+ header: React.ReactNode;
+}) {
+ const theme = useCustomTheme();
+ return (
+
+ {/* make the background semi-transparent */}
+
+
+ {/* header */}
+
+
+ {props.header}
+
+
+
+ {/* content */}
+
+ {props.children}
+
+
+ );
+}
+
+function ArrowSection() {
+ return (
+
+ );
+}
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/bridge-widget/bridge-widget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/bridge-widget/bridge-widget.tsx
index 74aa8d1c84a..fb29fe8bd97 100644
--- a/packages/thirdweb/src/react/web/ui/Bridge/bridge-widget/bridge-widget.tsx
+++ b/packages/thirdweb/src/react/web/ui/Bridge/bridge-widget/bridge-widget.tsx
@@ -149,15 +149,15 @@ export type BridgeWidgetProps = {
/**
* Configuration for the Buy tab. This mirrors {@link BuyWidget} options where applicable.
*/
- buy: {
+ buy?: {
/**
* The amount to buy (as a decimal string), e.g. "1.5" for 1.5 tokens.
*/
- amount: string; // TODO - make it optional
+ amount?: string;
/**
* The chain the accepted token is on.
*/
- chainId: number; // TODO - make it optional
+ chainId?: number;
/**
* Address of the token to buy. Leave undefined for the native token, or use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
*/
@@ -291,21 +291,23 @@ export function BridgeWidget(props: BridgeWidgetProps) {
{tab === "buy" && (
-
+
{(props.title || props.description) && (
<>
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/active-wallet-details.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/active-wallet-details.tsx
new file mode 100644
index 00000000000..cc1158b64f8
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/Bridge/common/active-wallet-details.tsx
@@ -0,0 +1,103 @@
+import styled from "@emotion/styled";
+import type { ThirdwebClient } from "../../../../../client/client.js";
+import { shortenAddress } from "../../../../../utils/address.js";
+import { AccountProvider } from "../../../../core/account/provider.js";
+import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
+import {
+ fontSize,
+ iconSize,
+ radius,
+ spacing,
+} from "../../../../core/design-system/index.js";
+import { WalletProvider } from "../../../../core/wallet/provider.js";
+import { Container } from "../../components/basic.js";
+import { Button } from "../../components/buttons.js";
+import { AccountAvatar } from "../../prebuilt/Account/avatar.js";
+import { AccountBlobbie } from "../../prebuilt/Account/blobbie.js";
+import { AccountName } from "../../prebuilt/Account/name.js";
+import { WalletIcon } from "../../prebuilt/Wallet/icon.js";
+import type { ActiveWalletInfo } from "../swap-widget/types.js";
+
+export function ActiveWalletDetails(props: {
+ activeWalletInfo: ActiveWalletInfo;
+ client: ThirdwebClient;
+ onClick: () => void;
+}) {
+ const wallet = props.activeWalletInfo.activeWallet;
+ const account = props.activeWalletInfo.activeAccount;
+
+ const accountBlobbie = (
+
+ );
+ const accountAvatarFallback = (
+
+ );
+
+ return (
+
+
+
+
+
+
+
+ {shortenAddress(account.address)}
+ }
+ loadingComponent={
+ {shortenAddress(account.address)}
+ }
+ />
+
+
+
+
+
+ );
+}
+
+const WalletButton = /* @__PURE__ */ styled(Button)(() => {
+ const theme = useCustomTheme();
+ return {
+ color: theme.colors.secondaryText,
+ transition: "color 200ms ease",
+ "&:hover": {
+ color: theme.colors.primaryText,
+ },
+ };
+});
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/decimal-input.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/decimal-input.tsx
new file mode 100644
index 00000000000..8b58b48b0ad
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/Bridge/common/decimal-input.tsx
@@ -0,0 +1,61 @@
+import { Input } from "../../components/formElements.js";
+
+type InputProps = React.JSX.IntrinsicElements["input"];
+
+export function DecimalInput(
+ props: Exclude<
+ InputProps,
+ "onChange" | "onClick" | "inputMode" | "pattern" | "type" | "value"
+ > & {
+ setValue: (value: string) => void;
+ },
+) {
+ const handleAmountChange = (inputValue: string) => {
+ let processedValue = inputValue;
+
+ // Replace comma with period if it exists
+ processedValue = processedValue.replace(",", ".");
+
+ if (processedValue.startsWith(".")) {
+ processedValue = `0${processedValue}`;
+ }
+
+ const numValue = Number(processedValue);
+ if (Number.isNaN(numValue)) {
+ return;
+ }
+
+ if (
+ processedValue.length > 1 &&
+ processedValue.startsWith("0") &&
+ !processedValue.startsWith("0.")
+ ) {
+ props.setValue(processedValue.slice(1));
+ } else {
+ props.setValue(processedValue);
+ }
+ };
+
+ return (
+ {
+ handleAmountChange(e.target.value);
+ }}
+ onClick={(e) => {
+ // put cursor at the end of the input
+ if (props.value === "") {
+ e.currentTarget.setSelectionRange(
+ e.currentTarget.value.length,
+ e.currentTarget.value.length,
+ );
+ }
+ }}
+ pattern="^[0-9]*[.,]?[0-9]*$"
+ placeholder="0.0"
+ type="text"
+ variant="transparent"
+ />
+ );
+}
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx
new file mode 100644
index 00000000000..7dbb4d26984
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx
@@ -0,0 +1,168 @@
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+import type { TokenWithPrices } from "../../../../../bridge/index.js";
+import type { BridgeChain } from "../../../../../bridge/types/Chain.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
+import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
+import {
+ fontSize,
+ iconSize,
+ radius,
+ spacing,
+} from "../../../../core/design-system/index.js";
+import { Container } from "../../components/basic.js";
+import { Button } from "../../components/buttons.js";
+import { Img } from "../../components/Img.js";
+import { Skeleton } from "../../components/Skeleton.js";
+import { Text } from "../../components/text.js";
+import { cleanedChainName } from "../swap-widget/utils.js";
+
+export function SelectedTokenButton(props: {
+ selectedToken:
+ | {
+ data: TokenWithPrices | undefined;
+ isFetching: boolean;
+ }
+ | undefined;
+ client: ThirdwebClient;
+ onSelectToken: () => void;
+ chain: BridgeChain | undefined;
+}) {
+ const theme = useCustomTheme();
+ return (
+
+ );
+}
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/token-balance.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/token-balance.tsx
new file mode 100644
index 00000000000..28d2337093b
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/Bridge/common/token-balance.tsx
@@ -0,0 +1,23 @@
+import { defineChain } from "../../../../../chains/utils.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { getAddress } from "../../../../../utils/address.js";
+import { useWalletBalance } from "../../../../core/hooks/others/useWalletBalance.js";
+
+export function useTokenBalance(props: {
+ chainId: number | undefined;
+ tokenAddress: string | undefined;
+ client: ThirdwebClient;
+ walletAddress: string | undefined;
+}) {
+ return useWalletBalance({
+ address: props.walletAddress,
+ chain: props.chainId ? defineChain(props.chainId) : undefined,
+ client: props.client,
+ tokenAddress: props.tokenAddress
+ ? getAddress(props.tokenAddress) === getAddress(NATIVE_TOKEN_ADDRESS)
+ ? undefined
+ : getAddress(props.tokenAddress)
+ : undefined,
+ });
+}
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/token-query.ts b/packages/thirdweb/src/react/web/ui/Bridge/common/token-query.ts
index 8c7c0bd7ae2..69cefd92dc0 100644
--- a/packages/thirdweb/src/react/web/ui/Bridge/common/token-query.ts
+++ b/packages/thirdweb/src/react/web/ui/Bridge/common/token-query.ts
@@ -12,11 +12,15 @@ type TokenQueryResult =
export function useTokenQuery(params: {
tokenAddress: string | undefined;
- chainId: number;
+ chainId: number | undefined;
client: ThirdwebClient;
}) {
return useQuery({
+ enabled: !!params.chainId,
queryFn: async (): Promise => {
+ if (!params.chainId) {
+ throw new Error("Chain ID is required");
+ }
const tokenAddress = params.tokenAddress || NATIVE_TOKEN_ADDRESS;
const token = await getToken(
params.client,
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx
index 57310bc2aeb..2f71d75c8f4 100644
--- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx
+++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/swap-ui.tsx
@@ -1,20 +1,14 @@
import styled from "@emotion/styled";
-import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import type { prepare as BuyPrepare } from "../../../../../bridge/Buy.js";
import { Buy, Sell } from "../../../../../bridge/index.js";
import type { prepare as SellPrepare } from "../../../../../bridge/Sell.js";
-import type { BridgeChain } from "../../../../../bridge/types/Chain.js";
import type { TokenWithPrices } from "../../../../../bridge/types/Token.js";
-import { defineChain } from "../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
-import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { getToken } from "../../../../../pay/convert/get-token.js";
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
-import { getAddress, shortenAddress } from "../../../../../utils/address.js";
import { toTokens, toUnits } from "../../../../../utils/units.js";
-import { AccountProvider } from "../../../../core/account/provider.js";
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
import {
fontSize,
@@ -23,9 +17,7 @@ import {
spacing,
type Theme,
} from "../../../../core/design-system/index.js";
-import { useWalletBalance } from "../../../../core/hooks/others/useWalletBalance.js";
import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js";
-import { WalletProvider } from "../../../../core/wallet/provider.js";
import { ConnectButton } from "../../ConnectWallet/ConnectButton.js";
import { DetailsModal } from "../../ConnectWallet/Details.js";
import { ArrowUpDownIcon } from "../../ConnectWallet/icons/ArrowUpDownIcon.js";
@@ -37,18 +29,16 @@ import {
} from "../../ConnectWallet/screens/formatTokenBalance.js";
import { Container } from "../../components/basic.js";
import { Button } from "../../components/buttons.js";
-import { Input } from "../../components/formElements.js";
-import { Img } from "../../components/Img.js";
import { Modal } from "../../components/Modal.js";
import { Skeleton } from "../../components/Skeleton.js";
import { Spacer } from "../../components/Spacer.js";
import { Spinner } from "../../components/Spinner.js";
import { Text } from "../../components/text.js";
import { useIsMobile } from "../../hooks/useisMobile.js";
-import { AccountAvatar } from "../../prebuilt/Account/avatar.js";
-import { AccountBlobbie } from "../../prebuilt/Account/blobbie.js";
-import { AccountName } from "../../prebuilt/Account/name.js";
-import { WalletIcon } from "../../prebuilt/Wallet/icon.js";
+import { ActiveWalletDetails } from "../common/active-wallet-details.js";
+import { DecimalInput } from "../common/decimal-input.js";
+import { SelectedTokenButton } from "../common/selected-token-button.js";
+import { useTokenBalance } from "../common/token-balance.js";
import { SelectToken } from "./select-token-ui.js";
import type {
ActiveWalletInfo,
@@ -57,7 +47,6 @@ import type {
TokenSelection,
} from "./types.js";
import { useBridgeChains } from "./use-bridge-chains.js";
-import { cleanedChainName } from "./utils.js";
type SwapUIProps = {
activeWalletInfo: ActiveWalletInfo | undefined;
@@ -423,7 +412,7 @@ export function SwapUI(props: SwapUIProps) {
label: "Swap",
style: {
width: "100%",
- borderRadius: radius.lg,
+ borderRadius: radius.full,
},
}}
theme={props.theme}
@@ -625,66 +614,6 @@ function useSwapQuote(params: {
});
}
-function DecimalInput(props: {
- value: string;
- setValue: (value: string) => void;
-}) {
- const handleAmountChange = (inputValue: string) => {
- let processedValue = inputValue;
-
- // Replace comma with period if it exists
- processedValue = processedValue.replace(",", ".");
-
- if (processedValue.startsWith(".")) {
- processedValue = `0${processedValue}`;
- }
-
- const numValue = Number(processedValue);
- if (Number.isNaN(numValue)) {
- return;
- }
-
- if (processedValue.startsWith("0") && !processedValue.startsWith("0.")) {
- props.setValue(processedValue.slice(1));
- } else {
- props.setValue(processedValue);
- }
- };
-
- return (
- {
- handleAmountChange(e.target.value);
- }}
- onClick={(e) => {
- // put cursor at the end of the input
- if (props.value === "") {
- e.currentTarget.setSelectionRange(
- e.currentTarget.value.length,
- e.currentTarget.value.length,
- );
- }
- }}
- pattern="^[0-9]*[.,]?[0-9]*$"
- placeholder="0.0"
- style={{
- border: "none",
- boxShadow: "none",
- fontSize: fontSize.xl,
- fontWeight: 500,
- paddingInline: 0,
- paddingBlock: 0,
- letterSpacing: "-0.025em",
- height: "30px",
- }}
- type="text"
- value={props.value}
- variant="transparent"
- />
- );
-}
-
function TokenSection(props: {
type: "buy" | "sell";
amount: {
@@ -774,19 +703,11 @@ function TokenSection(props: {
{props.activeWalletInfo && (
-
-
-
+ />
)}
@@ -829,6 +750,16 @@ function TokenSection(props: {
)}
@@ -914,157 +845,6 @@ function TokenSection(props: {
);
}
-function SelectedTokenButton(props: {
- selectedToken:
- | {
- data: TokenWithPrices | undefined;
- isFetching: boolean;
- }
- | undefined;
- client: ThirdwebClient;
- onSelectToken: () => void;
- chain: BridgeChain | undefined;
-}) {
- const theme = useCustomTheme();
- return (
-
- );
-}
-
function SwitchButton(props: { onClick: () => void }) {
return (
{
border: `1px solid ${theme.colors.borderColor}`,
};
});
-
-function useTokenBalance(props: {
- chainId: number | undefined;
- tokenAddress: string | undefined;
- client: ThirdwebClient;
- walletAddress: string | undefined;
-}) {
- return useWalletBalance({
- address: props.walletAddress,
- chain: props.chainId ? defineChain(props.chainId) : undefined,
- client: props.client,
- tokenAddress: props.tokenAddress
- ? getAddress(props.tokenAddress) === getAddress(NATIVE_TOKEN_ADDRESS)
- ? undefined
- : getAddress(props.tokenAddress)
- : undefined,
- });
-}
-
-function ActiveWalletDetails(props: {
- activeWalletInfo: ActiveWalletInfo;
- client: ThirdwebClient;
-}) {
- const wallet = props.activeWalletInfo.activeWallet;
- const account = props.activeWalletInfo.activeAccount;
-
- const accountBlobbie = (
-
- );
- const accountAvatarFallback = (
-
- );
-
- return (
-
-
-
-
-
-
-
- {shortenAddress(account.address)}
- }
- loadingComponent={
- {shortenAddress(account.address)}
- }
- />
-
-
-
-
-
- );
-}
-
-const WalletButton = /* @__PURE__ */ styled(Button)(() => {
- const theme = useCustomTheme();
- return {
- color: theme.colors.secondaryText,
- transition: "color 200ms ease",
- "&:hover": {
- color: theme.colors.primaryText,
- },
- };
-});
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/types.ts b/packages/thirdweb/src/react/web/ui/Bridge/types.ts
index 2f5929a7368..587a7acb326 100644
--- a/packages/thirdweb/src/react/web/ui/Bridge/types.ts
+++ b/packages/thirdweb/src/react/web/ui/Bridge/types.ts
@@ -1,4 +1,5 @@
import type { Quote, TokenWithPrices } from "../../../../bridge/index.js";
+import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
@@ -41,6 +42,6 @@ export type PaymentMethod =
| {
type: "fiat";
payerWallet?: Wallet;
- currency: string;
+ currency: SupportedFiatCurrency;
onramp: "stripe" | "coinbase" | "transak";
};
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
index 66ae3b42ae7..aa2f2121292 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
@@ -985,7 +985,6 @@ export function DetailsModal(props: {
chain={getCachedChain(requestedChainId)}
client={client}
hiddenWallets={props.detailsModal?.hiddenWallets}
- locale={locale.id}
connectOptions={props.connectOptions}
onCancel={() => setScreen("main")}
onSuccess={() => setScreen("main")}
@@ -1012,6 +1011,9 @@ export function DetailsModal(props: {
className="tw-modal__wallet-details"
title="Manage Wallet"
open={isOpen}
+ crossContainerStyles={{
+ display: screen === "buy" ? "none" : "block",
+ }}
setOpen={(_open) => {
if (!_open) {
closeModal();
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx
index 8c5a8a16245..f29463d4f3e 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/WalletRow.tsx
@@ -64,11 +64,7 @@ export function WalletRow(props: {
{props.label}
) : null}
-
+
{addressOrENS || shortenAddress(props.address)}
{profile.isLoading ? (
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts
index e8a107d7f74..0c9f9d450c4 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts
@@ -1,3 +1,4 @@
+import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
import { formatNumber } from "../../../../../utils/formatNumber.js";
import { toTokens } from "../../../../../utils/units.js";
@@ -33,7 +34,10 @@ export function formatTokenAmount(
).toString();
}
-export function formatCurrencyAmount(currency: string, amount: number) {
+export function formatCurrencyAmount(
+ currency: SupportedFiatCurrency,
+ amount: number,
+) {
return formatMoney(amount, "en-US", currency);
}
diff --git a/packages/thirdweb/src/react/web/ui/components/CopyIcon.tsx b/packages/thirdweb/src/react/web/ui/components/CopyIcon.tsx
index a8b38b99ddf..baf6b8eb6d4 100644
--- a/packages/thirdweb/src/react/web/ui/components/CopyIcon.tsx
+++ b/packages/thirdweb/src/react/web/ui/components/CopyIcon.tsx
@@ -13,6 +13,7 @@ export const CopyIcon: React.FC<{
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
hasCopied?: boolean;
+ iconSize?: number;
}> = (props) => {
const { hasCopied, onCopy } = useClipboard(props.text);
const showCheckIcon = props.hasCopied || hasCopied;
@@ -40,9 +41,17 @@ export const CopyIcon: React.FC<{
flex="row"
>
{showCheckIcon ? (
-
+
) : (
-
+
)}
diff --git a/packages/thirdweb/src/script-exports/bridge-widget-script.tsx b/packages/thirdweb/src/script-exports/bridge-widget-script.tsx
index 9eb6ab1b5b3..2647d82c492 100644
--- a/packages/thirdweb/src/script-exports/bridge-widget-script.tsx
+++ b/packages/thirdweb/src/script-exports/bridge-widget-script.tsx
@@ -41,9 +41,9 @@ export type BridgeWidgetScriptProps = {
};
};
};
- buy: {
- amount: string; // TODO - make it optional
- chainId: number; // TODO - make it optional
+ buy?: {
+ amount?: string;
+ chainId?: number;
tokenAddress?: string;
buttonLabel?: string;
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
diff --git a/packages/thirdweb/src/script-exports/readme.md b/packages/thirdweb/src/script-exports/readme.md
index 4b9d7e5715c..414845e13e6 100644
--- a/packages/thirdweb/src/script-exports/readme.md
+++ b/packages/thirdweb/src/script-exports/readme.md
@@ -21,10 +21,6 @@ Add the script in document head and the element where you want to render the bri
BridgeWidget.render(node, {
clientId: "your-client-id",
theme: "dark",
- buy: {
- chainId: 8453,
- amount: "0.1",
- },
});
```
@@ -41,10 +37,6 @@ Add the script in document head and the element where you want to render the bri
modalBg: "red",
},
},
- buy: {
- chainId: 8453,
- amount: "0.1",
- },
});
```
@@ -66,6 +58,7 @@ Add the script in document head and the element where you want to render the bri
buy: {
chainId: 8453,
amount: "0.1",
+ tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
},
});
diff --git a/packages/thirdweb/src/stories/BuyWidget.stories.tsx b/packages/thirdweb/src/stories/BuyWidget.stories.tsx
index be810debb6c..be11f5f4f0d 100644
--- a/packages/thirdweb/src/stories/BuyWidget.stories.tsx
+++ b/packages/thirdweb/src/stories/BuyWidget.stories.tsx
@@ -2,7 +2,10 @@ import type { Meta } from "@storybook/react-vite";
import { base } from "../chains/chain-definitions/base.js";
import { ethereum } from "../chains/chain-definitions/ethereum.js";
import { defineChain } from "../chains/utils.js";
-import { BuyWidget } from "../react/web/ui/Bridge/BuyWidget.js";
+import {
+ BuyWidget,
+ type BuyWidgetProps,
+} from "../react/web/ui/Bridge/BuyWidget.js";
import { storyClient } from "./utils.js";
const meta = {
@@ -13,13 +16,47 @@ const meta = {
} satisfies Meta;
export default meta;
+export function Basic() {
+ return ;
+}
+
+export function PayAnotherWallet() {
+ return (
+
+ );
+}
+
export function BuyBaseNativeToken() {
- return ;
+ return ;
+}
+
+export function JPYCurrency() {
+ return (
+
+ );
+}
+
+export function NoThirdwebBranding() {
+ return (
+
+ );
}
export function BuyBaseUSDC() {
return (
-
+
);
}
export function UnsupportedToken() {
return (
-
);
}
+
+function Variant(props: BuyWidgetProps) {
+ return (
+
+
+
+
+ );
+}