Skip to content

[SDK] Handle different fiat currencies in payment widgets #7578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ready-loops-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Handle different fiat currencies in payment widgets
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions apps/playground-web/src/app/connect/pay/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +5 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Export CURRENCIES array to enable reuse.

The currency list is comprehensive and well-structured. However, it should be exported to prevent duplication in other files like LeftSection.tsx.

-const CURRENCIES = [
+export const CURRENCIES = [
   "USD",
   "EUR",
   "GBP",
   // ... rest of currencies
 ] as const;

This will allow other components to import and use the same currency list, maintaining consistency across the codebase.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
export 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;
🤖 Prompt for AI Agents
In apps/playground-web/src/app/connect/pay/components/types.ts around lines 5 to
29, the CURRENCIES array is defined but not exported. To enable reuse and
maintain consistency across the codebase, add an export statement to the
CURRENCIES array so other files like LeftSection.tsx can import and use it
directly.


type SupportedFiatCurrency = (typeof CURRENCIES)[number] | (string & {});

export type BridgeComponentsPlaygroundOptions = {
theme: {
type: "dark" | "light";
Expand All @@ -26,6 +54,8 @@ export type BridgeComponentsPlaygroundOptions = {

paymentMethods: ("crypto" | "card")[];

currency?: SupportedFiatCurrency;

showThirdwebBranding: boolean;
};
};
52 changes: 52 additions & 0 deletions apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -134,6 +141,51 @@ export function LeftSection(props: {
/>
</section>

<section className="flex flex-col gap-3">
<Label htmlFor="currency">Display Currency</Label>
<Select
value={payOptions.currency || "USD"}
onValueChange={(value) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
currency: value,
},
}));
}}
>
<SelectTrigger>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="USD">USD - US Dollar</SelectItem>
<SelectItem value="EUR">EUR - Euro</SelectItem>
<SelectItem value="GBP">GBP - British Pound</SelectItem>
<SelectItem value="JPY">JPY - Japanese Yen</SelectItem>
<SelectItem value="KRW">KRW - Korean Won</SelectItem>
<SelectItem value="CNY">CNY - Chinese Yuan</SelectItem>
<SelectItem value="INR">INR - Indian Rupee</SelectItem>
<SelectItem value="NOK">NOK - Norwegian Krone</SelectItem>
<SelectItem value="SEK">SEK - Swedish Krona</SelectItem>
<SelectItem value="CHF">CHF - Swiss Franc</SelectItem>
<SelectItem value="AUD">AUD - Australian Dollar</SelectItem>
<SelectItem value="CAD">CAD - Canadian Dollar</SelectItem>
<SelectItem value="NZD">NZD - New Zealand Dollar</SelectItem>
<SelectItem value="MXN">MXN - Mexican Peso</SelectItem>
<SelectItem value="BRL">BRL - Brazilian Real</SelectItem>
<SelectItem value="CLP">CLP - Chilean Peso</SelectItem>
<SelectItem value="CZK">CZK - Czech Koruna</SelectItem>
<SelectItem value="DKK">DKK - Danish Krone</SelectItem>
<SelectItem value="HKD">HKD - Hong Kong Dollar</SelectItem>
<SelectItem value="HUF">HUF - Hungarian Forint</SelectItem>
<SelectItem value="IDR">IDR - Indonesian Rupiah</SelectItem>
<SelectItem value="ILS">ILS - Israeli Shekel</SelectItem>
<SelectItem value="ISK">ISK - Icelandic Krona</SelectItem>
</SelectContent>
</Select>
</section>

{/* Shared Chain and Token Selection - Always visible for Buy and Checkout modes */}
{(!payOptions.widget ||
payOptions.widget === "buy" ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
Expand All @@ -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}
/>
);
Expand All @@ -108,6 +110,7 @@ export function RightSection(props: {
to: account?.address || "",
tokenId: 2n,
})}
currency={props.options.payOptions.currency}
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
/>
);
Expand Down
1 change: 1 addition & 0 deletions apps/playground-web/src/app/connect/pay/embed/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = {
title: "",
transactionData: "",
widget: "buy",
currency: "USD",
showThirdwebBranding: true,
},
theme: {
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/bridge/types/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export type Token = {
symbol: string;
name: string;
iconUri?: string;
priceUsd: number;
prices: Record<string, number>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Breaking change: Token price structure updated.

The change from priceUsd: number to prices: Record<string, number> is a breaking change that enables multi-currency support. This will affect any code directly accessing the priceUsd property.

Verify that all consumers of the priceUsd property have been updated to use the new prices structure:


🏁 Script executed:

#!/bin/bash
# Description: Find any remaining references to priceUsd that might need updating
# Expected: Minimal or no results, indicating successful migration

echo "Searching for remaining priceUsd references..."
rg "priceUsd" --type ts --type tsx -A 2 -B 2

echo "Searching for direct property access patterns..."
rg "\.priceUsd" --type ts --type tsx -A 2 -B 2

Length of output: 359


🏁 Script executed:

#!/bin/bash
# Retry searching for any remaining references to priceUsd across TS and TSX files
echo "Searching for occurrences of priceUsd in .ts and .tsx files..."
rg "\bpriceUsd\b" -g "*.ts" -g "*.tsx" -n -C2

echo
echo "Searching for direct property access patterns (.priceUsd)..."
rg "\.priceUsd" -g "*.ts" -g "*.tsx" -n -C2

Length of output: 18312


Fix required: migrate all remaining priceUsd references to the new prices map

We’ve identified numerous leftover priceUsd: number usages that must be updated to use prices: Record<string, number>. Please update these locations accordingly:

• Story fixtures

  • packages/thirdweb/src/stories/Bridge/fixtures.ts
  • packages/thirdweb/src/stories/Bridge/SuccessScreen.stories.tsx
  • packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx
    • Core tests
  • packages/thirdweb/src/bridge/Token.test.ts
  • packages/thirdweb/src/bridge/Webhook.test.ts
  • packages/thirdweb/src/react/core/machines/paymentMachine.test.ts
  • packages/thirdweb/src/react/core/hooks/useBridgeRoutes.test.ts
    • Bridge UI components
  • packages/thirdweb/src/react/web/ui/Bridge/common/TokenAndChain.tsx
    • Backend schemas
  • packages/thirdweb/src/bridge/Webhook.ts
    • Dashboard utilities and components
  • apps/dashboard/.../public-pages/erc20/_utils/fetch-coin-info.ts
  • apps/dashboard/.../public-pages/erc20/_components/PriceChart.tsx

Ensure each priceUsd property is replaced by the appropriate lookup on prices, and adjust typing/schema definitions and tests to reflect the multi-currency structure.

🤖 Prompt for AI Agents
In packages/thirdweb/src/bridge/types/Token.ts at line 10, the property
priceUsd:number has been replaced by prices:Record<string, number>. You need to
update all remaining references to priceUsd across the listed files and
locations by replacing them with the appropriate access to the prices map, such
as prices["usd"] or another currency key. Also, update any typings, schema
definitions, fixtures, tests, and UI components to reflect this change from a
single number to a multi-currency prices map, ensuring all usages correctly
handle the new structure.

};
16 changes: 8 additions & 8 deletions packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -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,
Expand All @@ -348,7 +348,7 @@ export async function getBuyWithCryptoQuote(
firstStep.destinationToken.decimals,
),
) *
firstStep.destinationToken.priceUsd *
(firstStep.destinationToken.prices["USD"] || 0) *
100,
toAmountUSDCents:
Number(
Expand All @@ -357,7 +357,7 @@ export async function getBuyWithCryptoQuote(
firstStep.destinationToken.decimals,
),
) *
firstStep.destinationToken.priceUsd *
(firstStep.destinationToken.prices["USD"] || 0) *
100,
},
fromAddress: quote.intent.sender,
Expand All @@ -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,
},
Expand All @@ -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,
},
Expand Down
8 changes: 4 additions & 4 deletions packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -226,7 +226,7 @@ export async function getBuyWithCryptoTransfer(
firstStep.originToken.decimals,
),
) *
firstStep.originToken.priceUsd *
(firstStep.originToken.prices["USD"] || 0) *
100
: 0,
amountWei:
Expand All @@ -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,
},
Expand Down
8 changes: 4 additions & 4 deletions packages/thirdweb/src/pay/buyWithFiat/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,12 @@ export async function getBuyWithFiatQuote(
decimals: number;
symbol: string;
name: string;
priceUsd: number;
prices: Record<string, number>;
}): 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,
});
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand Down
8 changes: 4 additions & 4 deletions packages/thirdweb/src/pay/convert/cryptoToFiat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export type ConvertCryptoToFiatParams = {
chain: Chain;
/**
* The fiat symbol. e.g "USD"
* Only USD is supported at the moment.
*/
to: SupportedFiatCurrency;
};
Expand Down Expand Up @@ -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 };
}
Expand All @@ -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 };
}
8 changes: 4 additions & 4 deletions packages/thirdweb/src/pay/convert/fiatToCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type ConvertFiatToCryptoParams = {
client: ThirdwebClient;
/**
* The fiat symbol. e.g: "USD"
* Currently only USD is supported.
*/
from: SupportedFiatCurrency;
/**
Expand Down Expand Up @@ -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 };
}
Expand All @@ -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 };
}
Loading
Loading