Skip to content

Commit 15adec4

Browse files
[SDK] feat: Enhance fiat onramp with multi-step flow and currency support (#6323)
1 parent 1405289 commit 15adec4

29 files changed

+1146
-575
lines changed

.changeset/shaggy-flowers-argue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Fiat onramp UI revamp in PayEmbed and support multi hop onramp flows

apps/playground-web/src/app/connect/pay/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function StyledPayEmbed() {
4141
<>
4242
<div className="space-y-2">
4343
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
44-
Top Up
44+
Fund Wallet
4545
</h2>
4646
<p className="max-w-[600px]">
4747
Inline component that allows users to buy any currency.

apps/playground-web/src/app/navLinks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const staticSidebarLinks: SidebarLink[] = [
6565
expanded: false,
6666
links: [
6767
{
68-
name: "Top up",
68+
name: "Fund Wallet",
6969
href: "/connect/pay",
7070
},
7171
{

apps/playground-web/src/components/pay/embed.tsx

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,78 @@
22

33
import { THIRDWEB_CLIENT } from "@/lib/client";
44
import { useTheme } from "next-themes";
5-
import { base } from "thirdweb/chains";
6-
import { PayEmbed } from "thirdweb/react";
5+
import {
6+
arbitrum,
7+
arbitrumNova,
8+
base,
9+
defineChain,
10+
sepolia,
11+
treasure,
12+
} from "thirdweb/chains";
13+
import { PayEmbed, getDefaultToken } from "thirdweb/react";
714
import { StyledConnectButton } from "../styled-connect-button";
8-
915
export function StyledPayEmbedPreview() {
1016
const { theme } = useTheme();
1117

1218
return (
1319
<div className="flex flex-col items-center justify-center">
14-
<StyledConnectButton />
20+
<StyledConnectButton
21+
chains={[
22+
base,
23+
defineChain(466),
24+
arbitrum,
25+
treasure,
26+
arbitrumNova,
27+
sepolia,
28+
]}
29+
accountAbstraction={{
30+
chain: base,
31+
sponsorGas: true,
32+
}}
33+
supportedTokens={{
34+
466: [
35+
{
36+
address: "0x675C3ce7F43b00045a4Dab954AF36160fb57cB45",
37+
name: "USDC",
38+
symbol: "USDC",
39+
icon: getDefaultToken(base, "USDC")?.icon,
40+
},
41+
],
42+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
43+
8453: [getDefaultToken(base, "USDC")!],
44+
42161: [
45+
{
46+
address: "0x539bde0d7dbd336b79148aa742883198bbf60342",
47+
name: "MAGIC",
48+
symbol: "MAGIC",
49+
},
50+
],
51+
[arbitrumNova.id]: [
52+
{
53+
name: "Godcoin",
54+
symbol: "GOD",
55+
address: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948",
56+
icon: "https://assets.coingecko.com/coins/images/53848/standard/GodcoinTickerIcon_02.png",
57+
},
58+
],
59+
}}
60+
detailsButton={{
61+
displayBalanceToken: {
62+
466: "0x675C3ce7F43b00045a4Dab954AF36160fb57cB45",
63+
8453: getDefaultToken(base, "USDC")?.address ?? "",
64+
42161: "0x539bde0d7dbd336b79148aa742883198bbf60342",
65+
[arbitrumNova.id]: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948",
66+
},
67+
}}
68+
/>
1569
<div className="h-10" />
1670
<PayEmbed
71+
connectOptions={{
72+
accountAbstraction: {
73+
chain: base,
74+
sponsorGas: true,
75+
},
76+
}}
1777
client={THIRDWEB_CLIENT}
1878
theme={theme === "light" ? "light" : "dark"}
1979
payOptions={{

packages/thirdweb/src/pay/buyWithFiat/getQuote.ts

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { ThirdwebClient } from "../../client/client.js";
2+
import type { CurrencyMeta } from "../../react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.js";
23
import { getClientFetch } from "../../utils/fetch.js";
34
import { stringify } from "../../utils/json.js";
4-
import type { FiatProvider } from "../utils/commonTypes.js";
5+
import type { FiatProvider, PayTokenInfo } from "../utils/commonTypes.js";
56
import { getPayBuyWithFiatQuoteEndpoint } from "../utils/definitions.js";
6-
77
/**
88
* Parameters for [`getBuyWithFiatQuote`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatQuote) function
99
* @buyCrypto
@@ -40,7 +40,7 @@ export type GetBuyWithFiatQuoteParams = {
4040
/**
4141
* Symbol of the fiat currency to buy the token with.
4242
*/
43-
fromCurrencySymbol: "USD" | "CAD" | "GBP" | "EUR" | "JPY";
43+
fromCurrencySymbol: CurrencyMeta["shorthand"];
4444

4545
/**
4646
* The maximum slippage in basis points (bps) allowed for the transaction.
@@ -150,14 +150,7 @@ export type BuyWithFiatQuote = {
150150
/**
151151
* Token information for the desired token. (token the user wants to buy)
152152
*/
153-
toToken: {
154-
symbol?: string | undefined;
155-
priceUSDCents?: number | undefined;
156-
name?: string | undefined;
157-
chainId: number;
158-
tokenAddress: string;
159-
decimals: number;
160-
};
153+
toToken: PayTokenInfo;
161154
/**
162155
* Address of the wallet to which the tokens will be sent.
163156
*/
@@ -196,35 +189,17 @@ export type BuyWithFiatQuote = {
196189
amount: string;
197190
amountWei: string;
198191
amountUSDCents: number;
199-
token: {
200-
chainId: number;
201-
decimals: number;
202-
name: string;
203-
priceUSDCents: number;
204-
symbol: string;
205-
tokenAddress: string;
206-
};
192+
token: PayTokenInfo;
207193
};
208194

209195
/**
210-
* Gas Token that will be sent to the user's wallet address by the on-ramp provider.
211-
*
212-
* Only used for ERC20 + Gas on-ramp flow. This will hold the details of the gas token and amount sent for gas.
213-
*
214-
* In Native Currency case, extra for gas will be added to the output amount of the onramp.
196+
* Routing token that will be swapped from the on-ramp token, so that it can be bridged to the destination token.
215197
*/
216-
gasToken?: {
198+
routingToken?: {
217199
amount: string;
218200
amountWei: string;
219201
amountUSDCents: number;
220-
token: {
221-
chainId: number;
222-
decimals: number;
223-
name: string;
224-
priceUSDCents: number;
225-
symbol: string;
226-
tokenAddress: string;
227-
};
202+
token: PayTokenInfo;
228203
};
229204

230205
/**
@@ -318,6 +293,7 @@ export async function getBuyWithFiatQuote(
318293
fromAddress: params.fromAddress,
319294
toGasAmountWei: params.toGasAmountWei,
320295
preferredProvider: params.preferredProvider,
296+
multiHopSupported: true,
321297
}),
322298
});
323299

packages/thirdweb/src/pay/buyWithFiat/isSwapRequiredPostOnramp.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getAddress } from "../../utils/address.js";
2+
import type { PayTokenInfo } from "../utils/commonTypes.js";
23
import type { BuyWithFiatQuote } from "./getQuote.js";
34

45
/**
@@ -26,3 +27,58 @@ export function isSwapRequiredPostOnramp(
2627

2728
return !(sameChain && sameToken);
2829
}
30+
31+
export function getOnRampSteps(
32+
buyWithFiatQuote: BuyWithFiatQuote,
33+
): OnRampStep[] {
34+
const isSwapRequired = isSwapRequiredPostOnramp(buyWithFiatQuote);
35+
36+
if (!isSwapRequired) {
37+
return [
38+
{
39+
action: "buy",
40+
token: buyWithFiatQuote.toToken,
41+
amount: buyWithFiatQuote.estimatedToAmountMin,
42+
},
43+
];
44+
}
45+
46+
if (buyWithFiatQuote.routingToken) {
47+
return [
48+
{
49+
action: "buy",
50+
token: buyWithFiatQuote.onRampToken.token,
51+
amount: buyWithFiatQuote.onRampToken.amount,
52+
},
53+
{
54+
action: "swap",
55+
token: buyWithFiatQuote.routingToken.token,
56+
amount: buyWithFiatQuote.routingToken.amount,
57+
},
58+
{
59+
action: "bridge",
60+
token: buyWithFiatQuote.toToken,
61+
amount: buyWithFiatQuote.estimatedToAmountMin,
62+
},
63+
];
64+
}
65+
66+
return [
67+
{
68+
action: "buy",
69+
token: buyWithFiatQuote.onRampToken.token,
70+
amount: buyWithFiatQuote.onRampToken.amount,
71+
},
72+
{
73+
action: "swap",
74+
token: buyWithFiatQuote.toToken,
75+
amount: buyWithFiatQuote.estimatedToAmountMin,
76+
},
77+
];
78+
}
79+
80+
export type OnRampStep = {
81+
action: "buy" | "swap" | "bridge";
82+
token: PayTokenInfo;
83+
amount: string;
84+
};
Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
1-
const SUPPORTED_FIAT_CURRENCIES = ["USD"] as const;
1+
const SUPPORTED_FIAT_CURRENCIES = [
2+
"USD",
3+
"CAD",
4+
"GBP",
5+
"EUR",
6+
"JPY",
7+
"AUD",
8+
"NZD",
9+
] as const;
210
/**
311
* @internal
412
*/
513
export type SupportedFiatCurrency = (typeof SUPPORTED_FIAT_CURRENCIES)[number];
14+
15+
export function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) {
16+
switch (showBalanceInFiat) {
17+
case "USD":
18+
return "$";
19+
case "CAD":
20+
return "$";
21+
case "GBP":
22+
return "£";
23+
case "EUR":
24+
return "€";
25+
case "JPY":
26+
return "¥";
27+
case "AUD":
28+
return "$";
29+
case "NZD":
30+
return "$";
31+
default:
32+
return "$";
33+
}
34+
}

packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { SmartWalletOptions } from "../../../../wallets/smart/types.js";
1414
import type { AppMetadata } from "../../../../wallets/types.js";
1515
import type { WalletId } from "../../../../wallets/wallet-types.js";
1616
import type { NetworkSelectorProps } from "../../../web/ui/ConnectWallet/NetworkSelector.js";
17+
import type { CurrencyMeta } from "../../../web/ui/ConnectWallet/screens/Buy/fiat/currencies.js";
1718
import type { WelcomeScreen } from "../../../web/ui/ConnectWallet/screens/types.js";
1819
import type { LocaleId } from "../../../web/ui/types.js";
1920
import type { Theme } from "../../design-system/index.js";
@@ -90,7 +91,7 @@ export type PayUIOptions = Prettify<
9091
| {
9192
testMode?: boolean;
9293
prefillSource?: {
93-
currency?: "USD" | "CAD" | "GBP" | "EUR" | "JPY";
94+
currency?: CurrencyMeta["shorthand"];
9495
};
9596
preferredProvider?: FiatProvider;
9697
}

packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatStatus.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type GetBuyWithFiatStatusParams,
55
getBuyWithFiatStatus,
66
} from "../../../../pay/buyWithFiat/getStatus.js";
7+
import type { WithPickedOnceQueryOptions } from "../types.js";
78

89
/**
910
* A hook to get a status of a "Buy with Fiat" transaction to determine if the transaction is completed, failed or pending.
@@ -34,7 +35,7 @@ import {
3435
* @buyCrypto
3536
*/
3637
export function useBuyWithFiatStatus(
37-
params?: GetBuyWithFiatStatusParams,
38+
params?: WithPickedOnceQueryOptions<GetBuyWithFiatStatusParams>,
3839
): UseQueryResult<BuyWithFiatStatus> {
3940
return useQuery({
4041
queryKey: ["useBuyWithFiatStatus", params],
@@ -64,5 +65,6 @@ export function useBuyWithFiatStatus(
6465
},
6566
refetchIntervalInBackground: true,
6667
retry: true,
68+
...params?.queryOptions,
6769
});
6870
}

packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { useMutation } from "@tanstack/react-query";
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
22
import type { ThirdwebClient } from "../../../../client/client.js";
33
import { getContract } from "../../../../contract/contract.js";
44
import { resolveAddress } from "../../../../extensions/ens/resolve-address.js";
55
import { transfer } from "../../../../extensions/erc20/write/transfer.js";
66
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
7+
import { waitForReceipt } from "../../../../transaction/actions/wait-for-tx-receipt.js";
78
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
89
import { isAddress } from "../../../../utils/address.js";
910
import { isValidENSName } from "../../../../utils/ens/isValidENSName.js";
1011
import { toWei } from "../../../../utils/units.js";
12+
import { invalidateWalletBalance } from "../../providers/invalidateWalletBalance.js";
1113
import { useActiveWallet } from "./useActiveWallet.js";
1214

1315
/**
@@ -33,6 +35,7 @@ import { useActiveWallet } from "./useActiveWallet.js";
3335
*/
3436
export function useSendToken(client: ThirdwebClient) {
3537
const wallet = useActiveWallet();
38+
const queryClient = useQueryClient();
3639
return useMutation({
3740
async mutationFn(option: {
3841
tokenAddress?: string;
@@ -83,7 +86,7 @@ export function useSendToken(client: ThirdwebClient) {
8386
value: toWei(amount),
8487
});
8588

86-
await sendTransaction({
89+
return sendTransaction({
8790
transaction: sendNativeTokenTx,
8891
account,
8992
});
@@ -103,11 +106,24 @@ export function useSendToken(client: ThirdwebClient) {
103106
to,
104107
});
105108

106-
await sendTransaction({
109+
return sendTransaction({
107110
transaction: tx,
108111
account,
109112
});
110113
}
111114
},
115+
onSettled: async (data, error) => {
116+
if (error) {
117+
return;
118+
}
119+
if (data?.transactionHash) {
120+
await waitForReceipt({
121+
transactionHash: data.transactionHash,
122+
client,
123+
chain: data.chain,
124+
});
125+
}
126+
invalidateWalletBalance(queryClient);
127+
},
112128
});
113129
}

0 commit comments

Comments
 (0)