Skip to content

Commit

Permalink
[SDK] feat: Enhance fiat onramp with multi-step flow and currency sup…
Browse files Browse the repository at this point in the history
…port (#6323)
  • Loading branch information
joaquim-verges authored Feb 24, 2025
1 parent 1405289 commit 15adec4
Show file tree
Hide file tree
Showing 29 changed files with 1,146 additions and 575 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-flowers-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Fiat onramp UI revamp in PayEmbed and support multi hop onramp flows
2 changes: 1 addition & 1 deletion apps/playground-web/src/app/connect/pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function StyledPayEmbed() {
<>
<div className="space-y-2">
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
Top Up
Fund Wallet
</h2>
<p className="max-w-[600px]">
Inline component that allows users to buy any currency.
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-web/src/app/navLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const staticSidebarLinks: SidebarLink[] = [
expanded: false,
links: [
{
name: "Top up",
name: "Fund Wallet",
href: "/connect/pay",
},
{
Expand Down
68 changes: 64 additions & 4 deletions apps/playground-web/src/components/pay/embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,78 @@

import { THIRDWEB_CLIENT } from "@/lib/client";
import { useTheme } from "next-themes";
import { base } from "thirdweb/chains";
import { PayEmbed } from "thirdweb/react";
import {
arbitrum,
arbitrumNova,
base,
defineChain,
sepolia,
treasure,
} from "thirdweb/chains";
import { PayEmbed, getDefaultToken } from "thirdweb/react";
import { StyledConnectButton } from "../styled-connect-button";

export function StyledPayEmbedPreview() {
const { theme } = useTheme();

return (
<div className="flex flex-col items-center justify-center">
<StyledConnectButton />
<StyledConnectButton
chains={[
base,
defineChain(466),
arbitrum,
treasure,
arbitrumNova,
sepolia,
]}
accountAbstraction={{
chain: base,
sponsorGas: true,
}}
supportedTokens={{
466: [
{
address: "0x675C3ce7F43b00045a4Dab954AF36160fb57cB45",
name: "USDC",
symbol: "USDC",
icon: getDefaultToken(base, "USDC")?.icon,
},
],
// biome-ignore lint/style/noNonNullAssertion: <explanation>
8453: [getDefaultToken(base, "USDC")!],
42161: [
{
address: "0x539bde0d7dbd336b79148aa742883198bbf60342",
name: "MAGIC",
symbol: "MAGIC",
},
],
[arbitrumNova.id]: [
{
name: "Godcoin",
symbol: "GOD",
address: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948",
icon: "https://assets.coingecko.com/coins/images/53848/standard/GodcoinTickerIcon_02.png",
},
],
}}
detailsButton={{
displayBalanceToken: {
466: "0x675C3ce7F43b00045a4Dab954AF36160fb57cB45",
8453: getDefaultToken(base, "USDC")?.address ?? "",
42161: "0x539bde0d7dbd336b79148aa742883198bbf60342",
[arbitrumNova.id]: "0xb5130f4767ab0acc579f25a76e8f9e977cb3f948",
},
}}
/>
<div className="h-10" />
<PayEmbed
connectOptions={{
accountAbstraction: {
chain: base,
sponsorGas: true,
},
}}
client={THIRDWEB_CLIENT}
theme={theme === "light" ? "light" : "dark"}
payOptions={{
Expand Down
42 changes: 9 additions & 33 deletions packages/thirdweb/src/pay/buyWithFiat/getQuote.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ThirdwebClient } from "../../client/client.js";
import type { CurrencyMeta } from "../../react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.js";
import { getClientFetch } from "../../utils/fetch.js";
import { stringify } from "../../utils/json.js";
import type { FiatProvider } from "../utils/commonTypes.js";
import type { FiatProvider, PayTokenInfo } from "../utils/commonTypes.js";
import { getPayBuyWithFiatQuoteEndpoint } from "../utils/definitions.js";

/**
* Parameters for [`getBuyWithFiatQuote`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatQuote) function
* @buyCrypto
Expand Down Expand Up @@ -40,7 +40,7 @@ export type GetBuyWithFiatQuoteParams = {
/**
* Symbol of the fiat currency to buy the token with.
*/
fromCurrencySymbol: "USD" | "CAD" | "GBP" | "EUR" | "JPY";
fromCurrencySymbol: CurrencyMeta["shorthand"];

/**
* The maximum slippage in basis points (bps) allowed for the transaction.
Expand Down Expand Up @@ -150,14 +150,7 @@ export type BuyWithFiatQuote = {
/**
* Token information for the desired token. (token the user wants to buy)
*/
toToken: {
symbol?: string | undefined;
priceUSDCents?: number | undefined;
name?: string | undefined;
chainId: number;
tokenAddress: string;
decimals: number;
};
toToken: PayTokenInfo;
/**
* Address of the wallet to which the tokens will be sent.
*/
Expand Down Expand Up @@ -196,35 +189,17 @@ export type BuyWithFiatQuote = {
amount: string;
amountWei: string;
amountUSDCents: number;
token: {
chainId: number;
decimals: number;
name: string;
priceUSDCents: number;
symbol: string;
tokenAddress: string;
};
token: PayTokenInfo;
};

/**
* Gas Token that will be sent to the user's wallet address by the on-ramp provider.
*
* Only used for ERC20 + Gas on-ramp flow. This will hold the details of the gas token and amount sent for gas.
*
* In Native Currency case, extra for gas will be added to the output amount of the onramp.
* Routing token that will be swapped from the on-ramp token, so that it can be bridged to the destination token.
*/
gasToken?: {
routingToken?: {
amount: string;
amountWei: string;
amountUSDCents: number;
token: {
chainId: number;
decimals: number;
name: string;
priceUSDCents: number;
symbol: string;
tokenAddress: string;
};
token: PayTokenInfo;
};

/**
Expand Down Expand Up @@ -318,6 +293,7 @@ export async function getBuyWithFiatQuote(
fromAddress: params.fromAddress,
toGasAmountWei: params.toGasAmountWei,
preferredProvider: params.preferredProvider,
multiHopSupported: true,
}),
});

Expand Down
56 changes: 56 additions & 0 deletions packages/thirdweb/src/pay/buyWithFiat/isSwapRequiredPostOnramp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getAddress } from "../../utils/address.js";
import type { PayTokenInfo } from "../utils/commonTypes.js";
import type { BuyWithFiatQuote } from "./getQuote.js";

/**
Expand Down Expand Up @@ -26,3 +27,58 @@ export function isSwapRequiredPostOnramp(

return !(sameChain && sameToken);
}

export function getOnRampSteps(
buyWithFiatQuote: BuyWithFiatQuote,
): OnRampStep[] {
const isSwapRequired = isSwapRequiredPostOnramp(buyWithFiatQuote);

if (!isSwapRequired) {
return [
{
action: "buy",
token: buyWithFiatQuote.toToken,
amount: buyWithFiatQuote.estimatedToAmountMin,
},
];
}

if (buyWithFiatQuote.routingToken) {
return [
{
action: "buy",
token: buyWithFiatQuote.onRampToken.token,
amount: buyWithFiatQuote.onRampToken.amount,
},
{
action: "swap",
token: buyWithFiatQuote.routingToken.token,
amount: buyWithFiatQuote.routingToken.amount,
},
{
action: "bridge",
token: buyWithFiatQuote.toToken,
amount: buyWithFiatQuote.estimatedToAmountMin,
},
];
}

return [
{
action: "buy",
token: buyWithFiatQuote.onRampToken.token,
amount: buyWithFiatQuote.onRampToken.amount,
},
{
action: "swap",
token: buyWithFiatQuote.toToken,
amount: buyWithFiatQuote.estimatedToAmountMin,
},
];
}

export type OnRampStep = {
action: "buy" | "swap" | "bridge";
token: PayTokenInfo;
amount: string;
};
31 changes: 30 additions & 1 deletion packages/thirdweb/src/pay/convert/type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
const SUPPORTED_FIAT_CURRENCIES = ["USD"] as const;
const SUPPORTED_FIAT_CURRENCIES = [
"USD",
"CAD",
"GBP",
"EUR",
"JPY",
"AUD",
"NZD",
] as const;
/**
* @internal
*/
export type SupportedFiatCurrency = (typeof SUPPORTED_FIAT_CURRENCIES)[number];

export function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) {
switch (showBalanceInFiat) {
case "USD":
return "$";
case "CAD":
return "$";
case "GBP":
return "£";
case "EUR":
return "€";
case "JPY":
return "¥";
case "AUD":
return "$";
case "NZD":
return "$";
default:
return "$";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { SmartWalletOptions } from "../../../../wallets/smart/types.js";
import type { AppMetadata } from "../../../../wallets/types.js";
import type { WalletId } from "../../../../wallets/wallet-types.js";
import type { NetworkSelectorProps } from "../../../web/ui/ConnectWallet/NetworkSelector.js";
import type { CurrencyMeta } from "../../../web/ui/ConnectWallet/screens/Buy/fiat/currencies.js";
import type { WelcomeScreen } from "../../../web/ui/ConnectWallet/screens/types.js";
import type { LocaleId } from "../../../web/ui/types.js";
import type { Theme } from "../../design-system/index.js";
Expand Down Expand Up @@ -90,7 +91,7 @@ export type PayUIOptions = Prettify<
| {
testMode?: boolean;
prefillSource?: {
currency?: "USD" | "CAD" | "GBP" | "EUR" | "JPY";
currency?: CurrencyMeta["shorthand"];
};
preferredProvider?: FiatProvider;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type GetBuyWithFiatStatusParams,
getBuyWithFiatStatus,
} from "../../../../pay/buyWithFiat/getStatus.js";
import type { WithPickedOnceQueryOptions } from "../types.js";

/**
* A hook to get a status of a "Buy with Fiat" transaction to determine if the transaction is completed, failed or pending.
Expand Down Expand Up @@ -34,7 +35,7 @@ import {
* @buyCrypto
*/
export function useBuyWithFiatStatus(
params?: GetBuyWithFiatStatusParams,
params?: WithPickedOnceQueryOptions<GetBuyWithFiatStatusParams>,
): UseQueryResult<BuyWithFiatStatus> {
return useQuery({
queryKey: ["useBuyWithFiatStatus", params],
Expand Down Expand Up @@ -64,5 +65,6 @@ export function useBuyWithFiatStatus(
},
refetchIntervalInBackground: true,
retry: true,
...params?.queryOptions,
});
}
22 changes: 19 additions & 3 deletions packages/thirdweb/src/react/core/hooks/wallets/useSendToken.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getContract } from "../../../../contract/contract.js";
import { resolveAddress } from "../../../../extensions/ens/resolve-address.js";
import { transfer } from "../../../../extensions/erc20/write/transfer.js";
import { sendTransaction } from "../../../../transaction/actions/send-transaction.js";
import { waitForReceipt } from "../../../../transaction/actions/wait-for-tx-receipt.js";
import { prepareTransaction } from "../../../../transaction/prepare-transaction.js";
import { isAddress } from "../../../../utils/address.js";
import { isValidENSName } from "../../../../utils/ens/isValidENSName.js";
import { toWei } from "../../../../utils/units.js";
import { invalidateWalletBalance } from "../../providers/invalidateWalletBalance.js";
import { useActiveWallet } from "./useActiveWallet.js";

/**
Expand All @@ -33,6 +35,7 @@ import { useActiveWallet } from "./useActiveWallet.js";
*/
export function useSendToken(client: ThirdwebClient) {
const wallet = useActiveWallet();
const queryClient = useQueryClient();
return useMutation({
async mutationFn(option: {
tokenAddress?: string;
Expand Down Expand Up @@ -83,7 +86,7 @@ export function useSendToken(client: ThirdwebClient) {
value: toWei(amount),
});

await sendTransaction({
return sendTransaction({
transaction: sendNativeTokenTx,
account,
});
Expand All @@ -103,11 +106,24 @@ export function useSendToken(client: ThirdwebClient) {
to,
});

await sendTransaction({
return sendTransaction({
transaction: tx,
account,
});
}
},
onSettled: async (data, error) => {
if (error) {
return;
}
if (data?.transactionHash) {
await waitForReceipt({
transactionHash: data.transactionHash,
client,
chain: data.chain,
});
}
invalidateWalletBalance(queryClient);
},
});
}
Loading

0 comments on commit 15adec4

Please sign in to comment.