diff --git a/.changeset/slow-crews-thank.md b/.changeset/slow-crews-thank.md new file mode 100644 index 00000000000..1d477e260c7 --- /dev/null +++ b/.changeset/slow-crews-thank.md @@ -0,0 +1,11 @@ +--- +"thirdweb": patch +--- + +BuyWidget UI improvements and new features: + +- `chain`, and `amount` props are now optional +- User can always change the token and chain selection in the widget +- Both fiat and token amounts are editable +- connected wallet can be disconnected from the widget +- current balance displayed in the widget diff --git a/apps/playground-web/src/app/payments/commerce/CheckoutPlayground.tsx b/apps/playground-web/src/app/payments/commerce/CheckoutPlayground.tsx index 555df2a526f..b270b575265 100644 --- a/apps/playground-web/src/app/payments/commerce/CheckoutPlayground.tsx +++ b/apps/playground-web/src/app/payments/commerce/CheckoutPlayground.tsx @@ -18,6 +18,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = { sellerAddress: "0x0000000000000000000000000000000000000000", title: "", transactionData: "", + receiverAddress: undefined, currency: "USD", showThirdwebBranding: true, }, diff --git a/apps/playground-web/src/app/payments/components/CodeGen.tsx b/apps/playground-web/src/app/payments/components/CodeGen.tsx index aaa1b15ef51..14366a814a4 100644 --- a/apps/playground-web/src/app/payments/components/CodeGen.tsx +++ b/apps/playground-web/src/app/payments/components/CodeGen.tsx @@ -118,8 +118,12 @@ tokenId: 2n, tokenAddress: options.payOptions.buyTokenAddress ? quotes(options.payOptions.buyTokenAddress) : undefined, - seller: options.payOptions.sellerAddress - ? quotes(options.payOptions.sellerAddress) + seller: + widget === "checkout" + ? quotes(options.payOptions.sellerAddress) + : undefined, + receiverAddress: options.payOptions.receiverAddress + ? quotes(options.payOptions.receiverAddress) : undefined, buttonLabel: options.payOptions.buttonLabel ? quotes(options.payOptions.buttonLabel) diff --git a/apps/playground-web/src/app/payments/components/LeftSection.tsx b/apps/playground-web/src/app/payments/components/LeftSection.tsx index 6685c50e4cc..45ac1ca4239 100644 --- a/apps/playground-web/src/app/payments/components/LeftSection.tsx +++ b/apps/playground-web/src/app/payments/components/LeftSection.tsx @@ -140,6 +140,7 @@ export function LeftSection(props: {
+
{/* Buy Mode - Amount and Payment Methods */} {props.widget === "buy" && ( -
+
+ {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 ( +
+ + +
+ ); +}