diff --git a/.changeset/ready-loops-search.md b/.changeset/ready-loops-search.md new file mode 100644 index 00000000000..fc3230e9785 --- /dev/null +++ b/.changeset/ready-loops-search.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Handle different fiat currencies in payment widgets diff --git a/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx b/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx index d63b58c7f23..867ad8155d5 100644 --- a/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx +++ b/apps/playground-web/src/app/connect/pay/components/CodeGen.tsx @@ -73,7 +73,7 @@ function Example() { <${componentName} client={client} chain={defineChain(${options.payOptions.buyTokenChain.id})} - amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${options.payOptions.paymentMethods && options.payOptions.paymentMethods.length > 0 ? `\n\t paymentMethods={${JSON.stringify(options.payOptions.paymentMethods)}}` : ""}${ + amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${options.payOptions.paymentMethods && options.payOptions.paymentMethods.length > 0 ? `\n\t paymentMethods={${JSON.stringify(options.payOptions.paymentMethods)}}` : ""}${options.payOptions.currency ? `\n\t currency="${options.payOptions.currency}"` : ""}${ options.payOptions.widget === "transaction" ? `\n\t transaction={claimTo({ contract: nftContract, diff --git a/apps/playground-web/src/app/connect/pay/components/types.ts b/apps/playground-web/src/app/connect/pay/components/types.ts index fa4343e2194..f6713f34fed 100644 --- a/apps/playground-web/src/app/connect/pay/components/types.ts +++ b/apps/playground-web/src/app/connect/pay/components/types.ts @@ -2,6 +2,34 @@ import type { Chain } from "thirdweb/chains"; import type { ThemeOverrides } from "thirdweb/react"; import type { Address } from "thirdweb/utils"; +const CURRENCIES = [ + "USD", + "EUR", + "GBP", + "JPY", + "KRW", + "CNY", + "INR", + "NOK", + "SEK", + "CHF", + "AUD", + "CAD", + "NZD", + "MXN", + "BRL", + "CLP", + "CZK", + "DKK", + "HKD", + "HUF", + "IDR", + "ILS", + "ISK", +] as const; + +type SupportedFiatCurrency = (typeof CURRENCIES)[number] | (string & {}); + export type BridgeComponentsPlaygroundOptions = { theme: { type: "dark" | "light"; @@ -26,6 +54,8 @@ export type BridgeComponentsPlaygroundOptions = { paymentMethods: ("crypto" | "card")[]; + currency?: SupportedFiatCurrency; + showThirdwebBranding: boolean; }; }; diff --git a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx index ebeddba572e..920d2053809 100644 --- a/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx @@ -17,6 +17,13 @@ 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { TokenSelector } from "@/components/ui/TokenSelector"; import { THIRDWEB_CLIENT } from "@/lib/client"; import type { TokenMetadata } from "@/lib/types"; @@ -134,6 +141,51 @@ export function LeftSection(props: { /> +
+ + +
+ {/* Shared Chain and Token Selection - Always visible for Buy and Checkout modes */} {(!payOptions.widget || payOptions.widget === "buy" || diff --git a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx index d7726af45a5..d29b54806f6 100644 --- a/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/RightSection.tsx @@ -64,6 +64,7 @@ export function RightSection(props: { theme={themeObj} title={props.options.payOptions.title} tokenAddress={props.options.payOptions.buyTokenAddress} + currency={props.options.payOptions.currency} showThirdwebBranding={props.options.payOptions.showThirdwebBranding} /> ); @@ -88,6 +89,7 @@ export function RightSection(props: { seller={props.options.payOptions.sellerAddress} theme={themeObj} tokenAddress={props.options.payOptions.buyTokenAddress} + currency={props.options.payOptions.currency} showThirdwebBranding={props.options.payOptions.showThirdwebBranding} /> ); @@ -108,6 +110,7 @@ export function RightSection(props: { to: account?.address || "", tokenId: 2n, })} + currency={props.options.payOptions.currency} showThirdwebBranding={props.options.payOptions.showThirdwebBranding} /> ); diff --git a/apps/playground-web/src/app/connect/pay/embed/page.tsx b/apps/playground-web/src/app/connect/pay/embed/page.tsx index b555db63443..07e4e6ae3d7 100644 --- a/apps/playground-web/src/app/connect/pay/embed/page.tsx +++ b/apps/playground-web/src/app/connect/pay/embed/page.tsx @@ -18,6 +18,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = { title: "", transactionData: "", widget: "buy", + currency: "USD", showThirdwebBranding: true, }, theme: { diff --git a/packages/thirdweb/src/bridge/types/Token.ts b/packages/thirdweb/src/bridge/types/Token.ts index bf32dea7680..a54f0e9aaac 100644 --- a/packages/thirdweb/src/bridge/types/Token.ts +++ b/packages/thirdweb/src/bridge/types/Token.ts @@ -7,5 +7,5 @@ export type Token = { symbol: string; name: string; iconUri?: string; - priceUsd: number; + prices: Record; }; diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts index f8716d73747..f12041a6263 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts @@ -300,14 +300,14 @@ export async function getBuyWithCryptoQuote( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - firstStep.originToken.priceUsd * + (firstStep.originToken.prices["USD"] || 0) * 100, amountWei: quote.originAmount.toString(), token: { chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: firstStep.originToken.priceUsd * 100, + priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -323,7 +323,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: firstStep.originToken.priceUsd * 100, + priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -337,7 +337,7 @@ export async function getBuyWithCryptoQuote( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - firstStep.originToken.priceUsd * + (firstStep.originToken.prices["USD"] || 0) * 100, gasCostUSDCents: 0, slippageBPS: 0, @@ -348,7 +348,7 @@ export async function getBuyWithCryptoQuote( firstStep.destinationToken.decimals, ), ) * - firstStep.destinationToken.priceUsd * + (firstStep.destinationToken.prices["USD"] || 0) * 100, toAmountUSDCents: Number( @@ -357,7 +357,7 @@ export async function getBuyWithCryptoQuote( firstStep.destinationToken.decimals, ), ) * - firstStep.destinationToken.priceUsd * + (firstStep.destinationToken.prices["USD"] || 0) * 100, }, fromAddress: quote.intent.sender, @@ -372,7 +372,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: firstStep.originToken.priceUsd * 100, + priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -395,7 +395,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.destinationToken.chainId, decimals: firstStep.destinationToken.decimals, name: firstStep.destinationToken.name, - priceUSDCents: firstStep.destinationToken.priceUsd * 100, + priceUSDCents: (firstStep.destinationToken.prices["USD"] || 0) * 100, symbol: firstStep.destinationToken.symbol, tokenAddress: firstStep.destinationToken.address, }, diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts index 37952813f6d..3728cb9f357 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts @@ -198,14 +198,14 @@ export async function getBuyWithCryptoTransfer( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - firstStep.originToken.priceUsd * + (firstStep.originToken.prices["USD"] || 0) * 100, amountWei: quote.originAmount.toString(), token: { chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: firstStep.originToken.priceUsd * 100, + priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -226,7 +226,7 @@ export async function getBuyWithCryptoTransfer( firstStep.originToken.decimals, ), ) * - firstStep.originToken.priceUsd * + (firstStep.originToken.prices["USD"] || 0) * 100 : 0, amountWei: @@ -237,7 +237,7 @@ export async function getBuyWithCryptoTransfer( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: firstStep.originToken.priceUsd * 100, + priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index 578864edb4b..aa07953c97b 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -367,12 +367,12 @@ export async function getBuyWithFiatQuote( decimals: number; symbol: string; name: string; - priceUsd: number; + prices: Record; }): PayTokenInfo => ({ chainId: token.chainId, decimals: token.decimals, name: token.name, - priceUSDCents: Math.round(token.priceUsd * 100), + priceUSDCents: Math.round((token.prices["USD"] || 0) * 100), symbol: token.symbol, tokenAddress: token.address, }); @@ -408,7 +408,7 @@ export async function getBuyWithFiatQuote( const onRampTokenObject = { amount: onRampTokenAmount, amountUSDCents: Math.round( - Number(onRampTokenAmount) * onRampTokenRaw.priceUsd * 100, + Number(onRampTokenAmount) * (onRampTokenRaw.prices["USD"] || 0) * 100, ), amountWei: onRampTokenAmountWei.toString(), token: tokenToPayTokenInfo(onRampTokenRaw), @@ -434,7 +434,7 @@ export async function getBuyWithFiatQuote( routingTokenObject = { amount: routingAmount, amountUSDCents: Math.round( - Number(routingAmount) * routingTokenRaw.priceUsd * 100, + Number(routingAmount) * (routingTokenRaw.prices["USD"] || 0) * 100, ), amountWei: routingAmountWei.toString(), token: tokenToPayTokenInfo(routingTokenRaw), diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts index 6b80fd3a353..1c008b29b5b 100644 --- a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts +++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts @@ -26,7 +26,6 @@ export type ConvertCryptoToFiatParams = { chain: Chain; /** * The fiat symbol. e.g "USD" - * Only USD is supported at the moment. */ to: SupportedFiatCurrency; }; @@ -56,7 +55,7 @@ export type ConvertCryptoToFiatParams = { export async function convertCryptoToFiat( options: ConvertCryptoToFiatParams, ): Promise<{ result: number }> { - const { client, fromTokenAddress, chain, fromAmount } = options; + const { client, fromTokenAddress, chain, fromAmount, to } = options; if (Number(fromAmount) === 0) { return { result: 0 }; } @@ -74,10 +73,11 @@ export async function convertCryptoToFiat( ); } const token = await getToken(client, fromTokenAddress, chain.id); - if (token.priceUsd === 0) { + const price = token?.prices[to] || 0; + if (!token || price === 0) { throw new Error( `Error: Failed to fetch price for token ${fromTokenAddress} on chainId: ${chain.id}`, ); } - return { result: token.priceUsd * fromAmount }; + return { result: price * fromAmount }; } diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts index f0843f0974d..32958bdb306 100644 --- a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts +++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts @@ -13,7 +13,6 @@ export type ConvertFiatToCryptoParams = { client: ThirdwebClient; /** * The fiat symbol. e.g: "USD" - * Currently only USD is supported. */ from: SupportedFiatCurrency; /** @@ -57,7 +56,7 @@ export type ConvertFiatToCryptoParams = { export async function convertFiatToCrypto( options: ConvertFiatToCryptoParams, ): Promise<{ result: number }> { - const { client, to, chain, fromAmount } = options; + const { client, to, chain, fromAmount, from } = options; if (Number(fromAmount) === 0) { return { result: 0 }; } @@ -73,10 +72,11 @@ export async function convertFiatToCrypto( throw new Error("Invalid `to`. Expected a valid EVM contract address"); } const token = await getToken(client, to, chain.id); - if (!token || token.priceUsd === 0) { + const price = token?.prices[from] || 0; + if (!token || price === 0) { throw new Error( `Error: Failed to fetch price for token ${to} on chainId: ${chain.id}`, ); } - return { result: fromAmount / token.priceUsd }; + return { result: fromAmount / price }; } diff --git a/packages/thirdweb/src/pay/convert/type.ts b/packages/thirdweb/src/pay/convert/type.ts index b3a233ea987..62dd76cc0e0 100644 --- a/packages/thirdweb/src/pay/convert/type.ts +++ b/packages/thirdweb/src/pay/convert/type.ts @@ -1,16 +1,30 @@ -const SUPPORTED_FIAT_CURRENCIES = [ +const CURRENCIES = [ "USD", - "CAD", - "GBP", "EUR", + "GBP", "JPY", + "KRW", + "CNY", + "INR", + "NOK", + "SEK", + "CHF", "AUD", + "CAD", "NZD", + "MXN", + "BRL", + "CLP", + "CZK", + "DKK", + "HKD", + "HUF", + "IDR", + "ILS", + "ISK", ] as const; -/** - * @internal - */ -export type SupportedFiatCurrency = (typeof SUPPORTED_FIAT_CURRENCIES)[number]; + +export type SupportedFiatCurrency = (typeof CURRENCIES)[number] | (string & {}); export function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) { switch (showBalanceInFiat) { diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts index 2e9ae093c26..7898aead16f 100644 --- a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts @@ -106,7 +106,9 @@ export function usePaymentMethods(options: { decimals: b.decimals, iconUri: "", name: b.name, - priceUsd: 0, + prices: { + USD: 0, + }, symbol: b.symbol, } as Token, })); @@ -183,10 +185,10 @@ export function usePaymentMethods(options: { validOwnedTokens.sort((a, b) => { const aDollarBalance = Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) * - a.originToken.priceUsd; + (a.originToken.prices["USD"] || 0); const bDollarBalance = Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) * - b.originToken.priceUsd; + (b.originToken.prices["USD"] || 0); return bDollarBalance - aDollarBalance; }); diff --git a/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts index c3039109aec..64ec52d18d3 100644 --- a/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts +++ b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts @@ -9,6 +9,7 @@ import { getCompilerMetadata } from "../../../contract/actions/get-compiler-meta import { getContract } from "../../../contract/contract.js"; import { decimals } from "../../../extensions/erc20/read/decimals.js"; import { getToken } from "../../../pay/convert/get-token.js"; +import type { SupportedFiatCurrency } from "../../../pay/convert/type.js"; import { encode } from "../../../transaction/actions/encode.js"; import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js"; import { getTransactionGasCost } from "../../../transaction/utils.js"; @@ -43,6 +44,7 @@ interface UseTransactionDetailsOptions { transaction: PreparedTransaction; client: ThirdwebClient; wallet: Wallet | undefined; + currency?: SupportedFiatCurrency; } /** @@ -51,6 +53,7 @@ interface UseTransactionDetailsOptions { */ export function useTransactionDetails({ transaction, + currency, client, wallet, }: UseTransactionDetailsOptions) { @@ -153,9 +156,8 @@ export function useTransactionDetails({ : (value || 0n) + (gasCostWei || 0n); const totalCost = toTokens(totalCostWei, decimal); - const usdValue = tokenInfo?.priceUsd - ? Number(totalCost) * tokenInfo.priceUsd - : null; + const price = tokenInfo?.prices[currency || "USD"] || 0; + const usdValue = price ? Number(totalCost) * price : null; return { contractMetadata, @@ -170,7 +172,7 @@ export function useTransactionDetails({ totalCostWei, txCostDisplay: `${formatTokenAmount(costWei, decimal)} ${tokenSymbol}`, usdValueDisplay: usdValue - ? formatCurrencyAmount("USD", usdValue) + ? formatCurrencyAmount(currency || "USD", usdValue) : null, }; }, diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx index 5577beb3838..a01fe1d589e 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo } from "react"; import type { Token } from "../../../../bridge/types/Token.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 { PreparedTransaction } from "../../../../transaction/prepare-transaction.js"; import type { Address } from "../../../../utils/address.js"; @@ -39,6 +40,7 @@ export type UIOptions = Prettify< description?: string; image?: string; }; + currency?: SupportedFiatCurrency; } & ( | { mode: "fund_wallet"; @@ -310,6 +312,7 @@ export function BridgeOrchestrator({ onPaymentMethodSelected={handlePaymentMethodSelected} paymentMethods={paymentMethods} receiverAddress={state.context.receiverAddress} + currency={uiOptions.currency} /> )} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx index 2a48b03fba5..bbda75f9328 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx @@ -7,6 +7,7 @@ import type { Chain } from "../../../../chains/types.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 type { PurchaseData } from "../../../../pay/types.js"; import { type Address, @@ -173,6 +174,12 @@ export type BuyWidgetProps = { * @default ["crypto", "card"] */ paymentMethods?: ("crypto" | "card")[]; + + /** + * The currency to use for the payment. + * @default "USD" + */ + currency?: SupportedFiatCurrency; }; // Enhanced UIOptions to handle unsupported token state @@ -316,6 +323,7 @@ export function BuyWidget(props: BuyWidgetProps) { title: props.title, }, mode: "fund_wallet", + currency: props.currency || "USD", }, type: "success", }; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx index 6fc43d1a950..f7372ae756d 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx @@ -7,6 +7,7 @@ import type { Chain } from "../../../../chains/types.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 type { PurchaseData } from "../../../../pay/types.js"; import { type Address, checksumAddress } from "../../../../utils/address.js"; import { stringify } from "../../../../utils/json.js"; @@ -179,6 +180,12 @@ export type CheckoutWidgetProps = { * @default ["crypto", "card"] */ paymentMethods?: ("crypto" | "card")[]; + + /** + * The currency to use for the payment. + * @default "USD" + */ + currency?: SupportedFiatCurrency; }; // Enhanced UIOptions to handle unsupported token state @@ -302,6 +309,7 @@ export function CheckoutWidget(props: CheckoutWidgetProps) { title: props.name, }, mode: "direct_payment", + currency: props.currency || "USD", paymentInfo: { amount: props.amount, feePayer: props.feePayer === "seller" ? "receiver" : "sender", diff --git a/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx b/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx index c1405b7bfb1..a8be79d332a 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/DirectPayment.tsx @@ -2,10 +2,9 @@ import type { Token } from "../../../../bridge/types/Token.js"; import { defineChain } from "../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../client/client.js"; -import { type Address, shortenAddress } from "../../../../utils/address.js"; +import type { Address } from "../../../../utils/address.js"; import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; -import { useEnsName } from "../../../core/utils/wallet.js"; import { ConnectButton } from "../ConnectWallet/ConnectButton.js"; import { PoweredByThirdweb } from "../ConnectWallet/PoweredByTW.js"; import { FiatValue } from "../ConnectWallet/screens/Buy/swap/FiatValue.js"; @@ -64,12 +63,6 @@ export function DirectPayment({ uiOptions.paymentInfo.sellerAddress, ); }; - const ensName = useEnsName({ - address: uiOptions.paymentInfo.sellerAddress, - client, - }); - const sellerAddress = - ensName.data || shortenAddress(uiOptions.paymentInfo.sellerAddress); const buyNow = ( @@ -77,6 +70,7 @@ export function DirectPayment({ Buy Now · - {/* Seller section */} - - - Sold by - - - {sellerAddress} - - - - - { - if (uiOptions.destinationToken.priceUsd === 0) { + const price = + uiOptions.destinationToken.prices[uiOptions.currency || "USD"] || 0; + if (price === 0) { return; } // Convert USD amount to token amount using token price - const tokenAmount = usdAmount / uiOptions.destinationToken.priceUsd; + const tokenAmount = usdAmount / price; // Format to reasonable decimal places (up to 6 decimals, remove trailing zeros) const formattedAmount = numberToPlainString( Number.parseFloat(tokenAmount.toFixed(6)), @@ -228,9 +231,13 @@ export function FundWallet({ size="md" style={{ textWrap: "nowrap" }} > - ≈ $ - {(Number(amount) * uiOptions.destinationToken.priceUsd).toFixed( - 2, + ≈{" "} + {formatCurrencyAmount( + uiOptions.currency || "USD", + Number(amount) * + (uiOptions.destinationToken.prices[ + uiOptions.currency || "USD" + ] || 0), )} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx index 29d2d28ad86..830ab55a969 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx @@ -7,6 +7,7 @@ import type { Chain } from "../../../../chains/types.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 type { PurchaseData } from "../../../../pay/types.js"; import { type PreparedTransaction, @@ -182,6 +183,12 @@ export type TransactionWidgetProps = { * @default ["crypto", "card"] */ paymentMethods?: ("crypto" | "card")[]; + + /** + * The currency to use for the payment. + * @default "USD" + */ + currency?: SupportedFiatCurrency; }; // Enhanced UIOptions to handle unsupported token state @@ -341,6 +348,7 @@ export function TransactionWidget(props: TransactionWidgetProps) { return { data: { + currency: props.currency || "USD", metadata: { description: props.description, image: props.image, diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx index c51001a8081..84a77e39a61 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/common/TokenBalanceRow.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import type { Token } from "../../../../../bridge/index.js"; import { getCachedChain } from "../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; import { spacing } from "../../../../core/design-system/index.js"; import { FiatValue } from "../../ConnectWallet/screens/Buy/swap/FiatValue.js"; @@ -16,12 +17,14 @@ export function TokenBalanceRow({ amount, onClick, style, + currency, }: { client: ThirdwebClient; token: Token; amount: string; onClick: (token: Token) => void; style?: React.CSSProperties; + currency?: SupportedFiatCurrency; }) { const chain = getCachedChain(token.chainId); return ( @@ -66,6 +69,7 @@ export function TokenBalanceRow({ }} > {}} @@ -158,6 +159,7 @@ export function PaymentOverview(props: { style={{ alignItems: "flex-end" }} > {}} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx index a41d8984052..48eb6f17cd4 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx @@ -1,6 +1,7 @@ "use client"; import { useMemo } from "react"; import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; import { checksumAddress } from "../../../../../utils/address.js"; import { toTokens } from "../../../../../utils/units.js"; import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; @@ -24,6 +25,7 @@ interface FiatProviderSelectionProps { toTokenAddress: string; toAddress: string; toAmount?: string; + currency?: SupportedFiatCurrency; } const PROVIDERS = [ @@ -54,6 +56,7 @@ export function FiatProviderSelection({ toTokenAddress, toAddress, toAmount, + currency, }: FiatProviderSelectionProps) { const theme = useCustomTheme(); @@ -62,7 +65,7 @@ export function FiatProviderSelection({ amount: toAmount || "0", chainId: toChainId, client, - currency: "USD", + currency: currency || "USD", receiver: checksumAddress(toAddress), tokenAddress: checksumAddress(toTokenAddress), }); diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx index 59c872a4a67..1b87157ba5c 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx @@ -5,6 +5,7 @@ import { trackPayEvent } from "../../../../../analytics/track/pay.js"; import type { Token } from "../../../../../bridge/types/Token.js"; import { defineChain } from "../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js"; import type { Address } from "../../../../../utils/address.js"; import { toUnits } from "../../../../../utils/units.js"; import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; @@ -82,6 +83,12 @@ export interface PaymentSelectionProps { * Fee payer */ feePayer?: "sender" | "receiver"; + + /** + * The currency to use for the payment. + * @default "USD" + */ + currency?: SupportedFiatCurrency; } type Step = @@ -103,6 +110,7 @@ export function PaymentSelection({ includeDestinationToken, paymentMethods = ["crypto", "card"], feePayer, + currency, }: PaymentSelectionProps) { const connectedWallets = useConnectedWallets(); const activeWallet = useActiveWallet(); @@ -290,6 +298,7 @@ export function PaymentSelection({ toAmount={destinationAmount} toChainId={destinationToken.chainId} toTokenAddress={destinationToken.address} + currency={currency} /> )} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx index 5162bac27f9..721eb7999dc 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/FiatValue.tsx @@ -2,12 +2,13 @@ import { useQuery } from "@tanstack/react-query"; import type { Chain } from "../../../../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { convertCryptoToFiat } from "../../../../../../../pay/convert/cryptoToFiat.js"; -import { formatNumber } from "../../../../../../../utils/formatNumber.js"; +import type { SupportedFiatCurrency } from "../../../../../../../pay/convert/type.js"; import { fontSize } from "../../../../../../core/design-system/index.js"; import { Skeleton } from "../../../../components/Skeleton.js"; import type { TextProps } from "../../../../components/text.js"; import { Text } from "../../../../components/text.js"; import { useDebouncedValue } from "../../../../hooks/useDebouncedValue.js"; +import { formatCurrencyAmount } from "../../formatTokenBalance.js"; import type { ERC20OrNativeToken } from "../../nativeToken.js"; import { getTokenAddress } from "../../nativeToken.js"; @@ -17,6 +18,7 @@ export function FiatValue( token: ERC20OrNativeToken; chain: Chain; client: ThirdwebClient; + currency?: SupportedFiatCurrency; } & TextProps, ) { const deferredTokenAmount = useDebouncedValue(props.tokenAmount, 500); @@ -27,7 +29,7 @@ export function FiatValue( client: props.client, fromAmount: Number(deferredTokenAmount), fromTokenAddress: getTokenAddress(props.token), - to: "USD", + to: props.currency || "USD", }), queryKey: [ "cryptoToFiat", @@ -43,13 +45,10 @@ export function FiatValue( return cryptoToFiatQuery.data?.result ? ( - $ - {Number( - formatNumber(cryptoToFiatQuery.data.result, 2).toFixed(2), - ).toLocaleString(undefined, { - maximumFractionDigits: 2, - minimumFractionDigits: 2, - })} + {formatCurrencyAmount( + props.currency || "USD", + cryptoToFiatQuery.data.result, + )} ) : null; } 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 d5a9cca35d2..e8a107d7f74 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/formatTokenBalance.ts @@ -1,6 +1,5 @@ import { formatNumber } from "../../../../../utils/formatNumber.js"; import { toTokens } from "../../../../../utils/units.js"; -import { getCurrencyMeta } from "./Buy/fiat/currencies.js"; /** * @internal @@ -34,11 +33,17 @@ export function formatTokenAmount( ).toString(); } -export function formatCurrencyAmount( - currency: string, - amount: number, - decimals = 2, -) { - const symbol = getCurrencyMeta(currency).symbol; - return `${symbol}${formatNumber(amount, decimals).toFixed(decimals)}`; +export function formatCurrencyAmount(currency: string, amount: number) { + return formatMoney(amount, "en-US", currency); +} + +function formatMoney( + value: number, + locale: string, + currencyCode: string, +): string { + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currencyCode, + }).format(value); } diff --git a/packages/thirdweb/src/stories/Bridge/fixtures.ts b/packages/thirdweb/src/stories/Bridge/fixtures.ts index 79d9e034ebf..628660198c2 100644 --- a/packages/thirdweb/src/stories/Bridge/fixtures.ts +++ b/packages/thirdweb/src/stories/Bridge/fixtures.ts @@ -26,7 +26,9 @@ export const ETH: Token = { iconUri: "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png", name: "Ethereum", - priceUsd: 1000, + prices: { + USD: 1000, + }, symbol: "ETH", }; @@ -37,7 +39,9 @@ export const USDC: Token = { iconUri: "https://coin-images.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "USD Coin", - priceUsd: 1, + prices: { + USD: 1, + }, symbol: "USDC", }; @@ -48,7 +52,9 @@ export const UNI: Token = { iconUri: "https://coin-images.coingecko.com/coins/images/12504/large/uniswap-uni.png", name: "Uniswap", - priceUsd: 1000, + prices: { + USD: 1000, + }, symbol: "UNI", }; @@ -93,7 +99,9 @@ export const simpleOnrampQuote: BridgePrepareResult = JSON.parse( iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "USD Coin (PoS)", - priceUsd: 1.0, + prices: { + USD: 1.0, + }, symbol: "USDC", }, id: "onramp-simple-123", @@ -122,7 +130,9 @@ export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( chainId: 1, decimals: 18, name: "Ethereum", - priceUsd: 2500.0, + prices: { + USD: 2500.0, + }, symbol: "ETH", }, id: "onramp-swaps-456", @@ -143,7 +153,9 @@ export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( chainId: 137, decimals: 18, name: "Wrapped Ether", - priceUsd: 2500.0, + prices: { + USD: 2500.0, + }, symbol: "WETH", }, estimatedExecutionTimeMs: 30000, // 110 USDC @@ -155,7 +167,9 @@ export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "USD Coin (PoS)", - priceUsd: 1.0, + prices: { + USD: 1.0, + }, symbol: "USDC", }, transactions: [ @@ -186,7 +200,9 @@ export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( chainId: 1, decimals: 18, name: "Ethereum", - priceUsd: 2500.0, + prices: { + USD: 2500.0, + }, symbol: "ETH", }, estimatedExecutionTimeMs: 180000, // 0.044 WETH @@ -196,7 +212,9 @@ export const onrampWithSwapsQuote: BridgePrepareResult = JSON.parse( chainId: 137, decimals: 18, name: "Wrapped Ether", - priceUsd: 2500.0, + prices: { + USD: 2500.0, + }, symbol: "WETH", }, transactions: [ @@ -251,7 +269,9 @@ export const simpleBuyQuote: BridgePrepareResult = JSON.parse( iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "USD Coin", - priceUsd: 1.0, + prices: { + USD: 1.0, + }, symbol: "USDC", }, estimatedExecutionTimeMs: 60000, @@ -263,7 +283,9 @@ export const simpleBuyQuote: BridgePrepareResult = JSON.parse( iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "Ethereum", - priceUsd: 2500.0, + prices: { + USD: 2500.0, + }, symbol: "ETH", }, transactions: [ @@ -309,7 +331,9 @@ export const longTextBuyQuote: BridgePrepareResult = JSON.parse( iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", name: "USD Coin (USDC.e on Etherlink)", - priceUsd: 1.0, + prices: { + USD: 1.0, + }, symbol: "USDC.e", }, estimatedExecutionTimeMs: 60000,