From 5ce23c55a6063924d898e8b372468c482a48b21f Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 18 Apr 2024 01:01:39 +0530 Subject: [PATCH 001/135] [WIP] Add Buy with Credit Card UI --- packages/thirdweb/src/exports/pay.ts | 8 + packages/thirdweb/src/exports/react.ts | 2 + .../pay/buyWithCrypto/utils/definitions.ts | 15 ++ .../thirdweb/src/pay/buyWithFiat/getQuote.ts | 117 +++++++++++++ .../core/hooks/contract/useSendTransaction.ts | 8 +- .../core/hooks/pay/useBuyWithFiatQuote.ts | 21 +++ .../web/ui/ConnectWallet/icons/USDIcon.tsx | 43 +++++ .../screens/Buy/AccountSelectionScreen.tsx | 112 ------------ .../screens/Buy/AccountSelectorButton.tsx | 55 ------ .../ConnectWallet/screens/Buy/KadoScreen.tsx | 72 ++++++++ .../screens/Buy/PayWIthCreditCard.tsx | 84 +++++++++ .../screens/Buy/PaymentSelection.tsx | 40 ++--- .../ConnectWallet/screens/Buy/SwapScreen.tsx | 159 +++++++++++------- .../ConnectWallet/screens/Buy/swap/Fees.tsx | 32 ++++ .../screens/Buy/swap/SwapFees.tsx | 41 ----- .../swap/useBuyWithFiatSupportedCurrencies.ts | 22 +++ .../Buy/swap/useSwapSupportedChains.ts | 10 +- 17 files changed, 534 insertions(+), 307 deletions(-) create mode 100644 packages/thirdweb/src/pay/buyWithFiat/getQuote.ts create mode 100644 packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuote.ts create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/icons/USDIcon.tsx delete mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectionScreen.tsx delete mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectorButton.tsx create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/KadoScreen.tsx create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/PayWIthCreditCard.tsx delete mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFees.tsx create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useBuyWithFiatSupportedCurrencies.ts diff --git a/packages/thirdweb/src/exports/pay.ts b/packages/thirdweb/src/exports/pay.ts index 6b485a1eed0..f87c6040bff 100644 --- a/packages/thirdweb/src/exports/pay.ts +++ b/packages/thirdweb/src/exports/pay.ts @@ -18,3 +18,11 @@ export { type BuyWithCryptoHistoryData, type BuyWithCryptoHistoryParams, } from "../pay/buyWithCrypto/actions/getHistory.js"; + +// fiat ------------------------------------------------ + +export { + getBuyWithFiatQuote, + type BuyWithFiatQuote, + type GetBuyWithFiatQuoteParams, +} from "../pay/buyWithFiat/getQuote.js"; diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index 9677492b8bd..936d1d2b84c 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -96,3 +96,5 @@ export { AutoConnect, type AutoConnectProps, } from "../react/core/hooks/connection/useAutoConnect.js"; + +export { useBuyWithFiatQuote } from "../react/core/hooks/pay/useBuyWithFiatQuote.js"; diff --git a/packages/thirdweb/src/pay/buyWithCrypto/utils/definitions.ts b/packages/thirdweb/src/pay/buyWithCrypto/utils/definitions.ts index b93cd1f49d4..b859b57b582 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/utils/definitions.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/utils/definitions.ts @@ -15,6 +15,14 @@ export const getPayBuyWithCryptoStatusUrl = () => export const getPayBuyWithCryptoQuoteEndpoint = () => `https://${getThirdwebDomains().pay}/buy-with-crypto/quote/v1`; +/** + * Constructs the endpoint to get a pay quote. + * @param client - The Thirdweb client containing the baseUrl config + * @internal + */ +export const getPayBuyWithFiatQuoteEndpoint = () => + `https://${getThirdwebDomains().pay}/buy-with-fiat/quote/v1`; + /** * Constructs the endpoint to get a wallet address swap history. * @param client - The Thirdweb client containing the baseUrl config @@ -29,3 +37,10 @@ export const getPayBuyWithCryptoHistoryEndpoint = () => */ export const getPayChainsEndpoint = () => `https://${getThirdwebDomains().pay}/chains`; + +/** + * Constructs the endpoint to get the pay endpoint + * @internal + */ +export const getFiatCurrenciesEndpoint = () => + `https://${getThirdwebDomains().pay}/buy-with-fiat/currency/v1`; diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts new file mode 100644 index 00000000000..ce114068752 --- /dev/null +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -0,0 +1,117 @@ +import type { ThirdwebClient } from "../../client/client.js"; +import { getClientFetch } from "../../utils/fetch.js"; +import { getPayBuyWithFiatQuoteEndpoint } from "../buyWithCrypto/utils/definitions.js"; + +// TODO: VERIFY THE TYPES !!!! + +// TODO: add JSDoc description for all properties + +/** + * TODO + */ +export type GetBuyWithFiatQuoteParams = { + client: ThirdwebClient; + // required + fromAddress: string; + toAddress: string; + toChainId: number; + toTokenAddress: string; + fromCurrencySymbol: string; + + // optional + maxSlippageBPS?: number | undefined; + fromAmount?: string | undefined; + toAmount?: string | undefined; +}; + +export type BuyWithFiatQuote = { + estimatedDurationSeconds: number; + estimatedToAmountMin: string; + estimatedToAmountMinWei: string; + fromCurrency: { + amount: string; + amountUnits: string; + decimals: number; + currencySymbol: string; + }; + toToken: { + symbol?: string | undefined; + priceUSDCents?: number | undefined; + name?: string | undefined; + chainId: number; + tokenAddress: string; + decimals: number; + }; + toAddress: string; + maxSlippageBPS: number; + quoteId: string; + toAmountMinWei: string; + toAmountMin: string; + processingFees: { + amount: string; + amountUnits: string; + decimals: number; + currencySymbol: string; + feeType: "ON_RAMP" | "NETWORK"; + }[]; + + onRampLink: string; +}; + +/** + * TODO + * @buyFiat + */ +export async function getBuyWithFiatQuote( + params: GetBuyWithFiatQuoteParams, +): Promise { + try { + const queryParams = new URLSearchParams({ + fromAddress: params.fromAddress, + toAddress: params.toAddress, + fromCurrencySymbol: params.fromCurrencySymbol, + toChainId: params.toChainId.toString(), + toTokenAddress: params.toTokenAddress.toLowerCase(), + }); + + if (params.fromAmount) { + queryParams.append("fromAmount", params.fromAmount); + } + + if (params.toAmount) { + queryParams.append("toAmount", params.toAmount); + } + + if (params.maxSlippageBPS) { + queryParams.append("maxSlippageBPS", params.maxSlippageBPS.toString()); + } + + const queryString = queryParams.toString(); + const url = `${getPayBuyWithFiatQuoteEndpoint()}?${queryString}`; + + const response = await getClientFetch(params.client)(url); + + // Assuming the response directly matches the SwapResponse interface + if (!response.ok) { + const errorObj = await response.json(); + if ( + errorObj && + "error" in errorObj && + typeof errorObj.error === "object" && + "message" in errorObj.error + ) { + throw new Error(errorObj.error.message); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = (await response.json()).result; + + console.log(data); + + return data; + } catch (error) { + console.error("Fetch error:", error); + throw new Error(`Fetch failed: ${error}`); + } +} diff --git a/packages/thirdweb/src/react/core/hooks/contract/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/contract/useSendTransaction.ts index 278869e71c6..5c7533439f7 100644 --- a/packages/thirdweb/src/react/core/hooks/contract/useSendTransaction.ts +++ b/packages/thirdweb/src/react/core/hooks/contract/useSendTransaction.ts @@ -6,7 +6,7 @@ import { sendTransaction } from "../../../../transaction/actions/send-transactio import type { WaitForReceiptOptions } from "../../../../transaction/actions/wait-for-tx-receipt.js"; import type { PreparedTransaction } from "../../../../transaction/prepare-transaction.js"; import type { GetWalletBalanceResult } from "../../../../wallets/utils/getWalletBalance.js"; -import { fetchSwapSupportedChains } from "../../../web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.js"; +import { fetchBuySupportedChains } from "../../../web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.js"; import { useActiveAccount } from "../wallets/wallet-hooks.js"; type ShowModalData = { @@ -64,11 +64,9 @@ export function useSendTransactionCore( (async () => { try { - const swapSupportedChains = await fetchSwapSupportedChains( - tx.client, - ); + const buySupportedChains = await fetchBuySupportedChains(tx.client); - const isBuySupported = swapSupportedChains.find( + const isBuySupported = buySupportedChains.find( (c) => c.id === tx.chain.id, ); diff --git a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuote.ts b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuote.ts new file mode 100644 index 00000000000..2a5f64ed060 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuote.ts @@ -0,0 +1,21 @@ +import { useQuery } from "@tanstack/react-query"; +import { + type GetBuyWithFiatQuoteParams, + getBuyWithFiatQuote, +} from "../../../../pay/buyWithFiat/getQuote.js"; + +/** + * TODO + */ +export function useBuyWithFiatQuote(params?: GetBuyWithFiatQuoteParams) { + return useQuery({ + queryKey: ["useBuyWithFiatQuote", params], + queryFn: async () => { + if (!params) { + throw new Error("No params provided"); + } + return getBuyWithFiatQuote(params); + }, + enabled: !!params, + }); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/USDIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/USDIcon.tsx new file mode 100644 index 00000000000..2c9a2c3cd6c --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/USDIcon.tsx @@ -0,0 +1,43 @@ +import type { IconFC } from "./types.js"; + +/** + * @internal + */ +export const USDIcon: IconFC = (props) => { + return ( + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectionScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectionScreen.tsx deleted file mode 100644 index 74ef66138ad..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectionScreen.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import styled from "@emotion/styled"; -import { useMemo, useState } from "react"; -import type { ThirdwebClient } from "../../../../../../client/client.js"; -import { isAddress } from "../../../../../../utils/address.js"; -import type { - Account, - Wallet, -} from "../../../../../../wallets/interfaces/wallet.js"; -import { Spacer } from "../../../components/Spacer.js"; -import { Container } from "../../../components/basic.js"; -import { Button } from "../../../components/buttons.js"; -import { Input } from "../../../components/formElements.js"; -import { Text } from "../../../components/text.js"; -import { useCustomTheme } from "../../../design-system/CustomThemeProvider.js"; -import { AccountSelectorButton } from "./AccountSelectorButton.js"; - -/** - * @internal - */ -export function AccountSelectionScreen(props: { - activeWallet: Wallet; - activeAccount: Account; - onSelect: (address: string) => void; - client: ThirdwebClient; -}) { - const [address, setAddress] = useState(""); - const isValidAddress = useMemo(() => isAddress(address), [address]); - const showError = !!address && !isValidAddress; - - return ( -
- - Send to - - - - setAddress(e.target.value)} - /> - - - - {showError && ( - <> - - - Invalid address - - - )} - - - Connected - - { - props.onSelect(props.activeAccount.address); - }} - /> -
- ); -} - -const StyledInput = /* @__PURE__ */ styled(Input)(() => { - const theme = useCustomTheme(); - return { - border: `1.5px solid ${theme.colors.borderColor}`, - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - height: "100%", - boxSizing: "border-box", - boxShadow: "none", - borderRight: "none", - "&:focus": { - boxShadow: "none", - borderColor: theme.colors.accentText, - }, - "&[data-is-error='true']": { - boxShadow: "none", - borderColor: theme.colors.danger, - }, - }; -}); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectorButton.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectorButton.tsx deleted file mode 100644 index 3b95aaca220..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/AccountSelectorButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { ChevronDownIcon } from "@radix-ui/react-icons"; -import type { ThirdwebClient } from "../../../../../../client/client.js"; -import type { - Account, - Wallet, -} from "../../../../../../wallets/interfaces/wallet.js"; -import { shortenString } from "../../../../../core/utils/addresses.js"; -import { WalletImage } from "../../../components/WalletImage.js"; -import { Container } from "../../../components/basic.js"; -import { Text } from "../../../components/text.js"; -import { iconSize } from "../../../design-system/index.js"; -import { WalletIcon } from "../../icons/WalletIcon.js"; -import { SecondaryButton } from "./buttons.js"; - -/** - * - * @internal - */ -export function AccountSelectorButton(props: { - onClick: () => void; - activeWallet: Wallet; - activeAccount: Account; - address: string; - chevron?: boolean; - client: ThirdwebClient; -}) { - return ( - - {props.activeAccount.address === props.address ? ( - - ) : ( - - - - )} - - - {shortenString(props.address, false)} - - {props.chevron && ( - - )} - - ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/KadoScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/KadoScreen.tsx new file mode 100644 index 00000000000..c8abe28af55 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/KadoScreen.tsx @@ -0,0 +1,72 @@ +import { useEffect } from "react"; +import type { BuyWithFiatQuote } from "../../../../../../pay/buyWithFiat/getQuote.js"; +import { Spinner } from "../../../components/Spinner.js"; +import { Container, Line, ModalHeader } from "../../../components/basic.js"; +import { radius } from "../../../design-system/index.js"; + +/** + * @internal + */ +export function KadoScreen(props: { + quote: BuyWithFiatQuote; + onBack: () => void; +}) { + const iframeSrc = props.quote.onRampLink; + const iframeOrigin = new URL(iframeSrc).origin; + + useEffect(() => { + function handlePostMessage(event: MessageEvent) { + if (event.origin !== iframeOrigin) { + return; + } + + // TODO + console.log("got from kado", event); + } + + window.addEventListener("message", handlePostMessage); + return () => { + window.removeEventListener("message", handlePostMessage); + }; + }, [iframeOrigin]); + + return ( + + + + + +
+
+ +
+