diff --git a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx index 10d4c008f..4f1f67e65 100644 --- a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx +++ b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx @@ -56,7 +56,9 @@ export type SequenceCheckoutProviderProps = { const getDefaultLocationCheckout = (): NavigationCheckout => { return { location: 'payment-method-selection', - params: {} + params: { + isInitialBalanceChecked: false + } } } export const SequenceCheckoutProvider = ({ children, config }: SequenceCheckoutProviderProps) => { diff --git a/packages/checkout/src/contexts/NavigationCheckout.ts b/packages/checkout/src/contexts/NavigationCheckout.ts index d9d6d5ba3..0b485d1ec 100644 --- a/packages/checkout/src/contexts/NavigationCheckout.ts +++ b/packages/checkout/src/contexts/NavigationCheckout.ts @@ -7,6 +7,7 @@ export interface PaymentMethodSelectionParams { address: string chainId: number } + isInitialBalanceChecked: boolean } export interface PaymentMehodSelection { diff --git a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx index e222175c9..b0deae4ab 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx @@ -24,11 +24,14 @@ import { useAccount, useChainId, usePublicClient, useReadContract, useSwitchChai import { ERC_20_CONTRACT_ABI } from '../../../../constants/abi.js' import { EVENT_SOURCE } from '../../../../constants/index.js' +import { type PaymentMethodSelectionParams } from '../../../../contexts/NavigationCheckout.js' import type { SelectPaymentSettings } from '../../../../contexts/SelectPaymentModal.js' import { useAddFundsModal } from '../../../../hooks/index.js' import { useSelectPaymentModal, useTransactionStatusModal } from '../../../../hooks/index.js' import { useNavigationCheckout } from '../../../../hooks/useNavigationCheckout.js' +import { useInitialBalanceCheck } from './useInitialBalanceCheck.js' + interface PayWithCryptoTabProps { skipOnCloseCallback: () => void isSwitchingChainRef: RefObject @@ -156,7 +159,8 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P const isNotEnoughBalanceError = typeof swapQuoteError?.cause === 'string' && swapQuoteError?.cause?.includes('not enough balance for swap') - const selectedCurrencyPrice = isSwapTransaction ? swapQuote?.maxPrice || 0 : price || 0 + const maxPrice = swapQuote?.maxPrice && swapQuote.maxPrice !== '' ? swapQuote.maxPrice : 0 + const selectedCurrencyPrice = isSwapTransaction ? maxPrice : price || 0 const { data: allowanceData, isLoading: allowanceIsLoading } = useReadContract({ abi: ERC_20_CONTRACT_ABI, @@ -169,20 +173,33 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P } }) + const isInitialBalanceChecked = (navigation.params as PaymentMethodSelectionParams).isInitialBalanceChecked + const isLoading = isLoadingCoinPrice || isLoadingCurrencyInfo || (allowanceIsLoading && !isNativeToken) || isLoadingSwapQuote || tokenBalancesIsLoading || - isLoadingSelectedCurrencyInfo + isLoadingSelectedCurrencyInfo || + !isInitialBalanceChecked const tokenBalance = tokenBalancesData?.pages?.[0]?.balances?.find(balance => compareAddress(balance.contractAddress, selectedCurrency.address) ) const isInsufficientBalance = - tokenBalance === undefined || (tokenBalance?.balance && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) + tokenBalance === undefined || + (tokenBalance?.balance && tokenBalance.balance !== '' && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) + + useInitialBalanceCheck({ + userAddress: userAddress || '', + buyCurrencyAddress, + price, + chainId, + isInsufficientBalance: isInsufficientBalance as boolean, + tokenBalancesIsLoading + }) const isApproved: boolean = (allowanceData as bigint) >= BigInt(price) || isNativeToken diff --git a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/useInitialBalanceCheck.tsx b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/useInitialBalanceCheck.tsx new file mode 100644 index 000000000..61a786c8e --- /dev/null +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/useInitialBalanceCheck.tsx @@ -0,0 +1,117 @@ +import { compareAddress, ContractVerificationStatus } from '@0xsequence/connect' +import { useGetSwapRoutes, useGetTokenBalancesSummary } from '@0xsequence/hooks' +import { useEffect } from 'react' +import { zeroAddress } from 'viem' + +import { type PaymentMethodSelectionParams } from '../../../../contexts/NavigationCheckout.js' +import { useNavigationCheckout } from '../../../../hooks/useNavigationCheckout.js' + +interface UseInitialBalanceCheckArgs { + userAddress: string + buyCurrencyAddress: string + price: string + chainId: number + isInsufficientBalance: boolean + tokenBalancesIsLoading: boolean +} + +// Hook to check if the user has enough of a balance of the +// initial currency to purchase the item +// If not, a swap route for which he has enough balance will be selected +export const useInitialBalanceCheck = ({ + userAddress, + buyCurrencyAddress, + price, + chainId, + isInsufficientBalance, + tokenBalancesIsLoading +}: UseInitialBalanceCheckArgs) => { + const { navigation, setNavigation } = useNavigationCheckout() + + const isInitialBalanceChecked = (navigation.params as PaymentMethodSelectionParams).isInitialBalanceChecked + + const { data: swapRoutes = [], isLoading: swapRoutesIsLoading } = useGetSwapRoutes( + { + walletAddress: userAddress ?? '', + toTokenAddress: buyCurrencyAddress, + toTokenAmount: price, + chainId: chainId + }, + { + disabled: isInitialBalanceChecked || !isInsufficientBalance + } + ) + + const { data: swapRoutesTokenBalancesData, isLoading: swapRoutesTokenBalancesIsLoading } = useGetTokenBalancesSummary( + { + chainIds: [chainId], + filter: { + accountAddresses: userAddress ? [userAddress] : [], + contractStatus: ContractVerificationStatus.ALL, + contractWhitelist: swapRoutes + .flatMap(route => route.fromTokens) + .map(token => token.address) + .filter(address => compareAddress(address, zeroAddress)), + omitNativeBalances: false + }, + omitMetadata: true + }, + { + disabled: isInitialBalanceChecked || !isInsufficientBalance || swapRoutesIsLoading + } + ) + + const findSwapQuote = async () => { + let validSwapRoute: string | undefined + + const route = swapRoutes[0] + for (let j = 0; j < route.fromTokens.length; j++) { + const fromToken = route.fromTokens[j] + const balance = swapRoutesTokenBalancesData?.pages?.[0]?.balances?.find(balance => + compareAddress(balance.contractAddress, fromToken.address) + ) + + if (!balance) { + continue + } + if (BigInt(balance.balance || '0') >= BigInt(fromToken.price || '0')) { + validSwapRoute = fromToken.address + break + } + } + + setNavigation({ + location: 'payment-method-selection', + params: { + ...navigation.params, + selectedCurrency: { + address: validSwapRoute || buyCurrencyAddress, + chainId: chainId + }, + isInitialBalanceChecked: true + } + }) + } + + useEffect(() => { + if (!isInitialBalanceChecked && !tokenBalancesIsLoading && !swapRoutesIsLoading && !swapRoutesTokenBalancesIsLoading) { + if (isInsufficientBalance) { + findSwapQuote() + } else { + setNavigation({ + location: 'payment-method-selection', + params: { + ...navigation.params, + isInitialBalanceChecked: true + } + }) + } + } + }, [ + isInitialBalanceChecked, + isInsufficientBalance, + tokenBalancesIsLoading, + swapRoutesIsLoading, + swapRoutesTokenBalancesIsLoading + ]) +} diff --git a/packages/checkout/src/views/Checkout/TokenSelection/index.tsx b/packages/checkout/src/views/Checkout/TokenSelection/index.tsx index f8dc2984f..ee80a9fab 100644 --- a/packages/checkout/src/views/Checkout/TokenSelection/index.tsx +++ b/packages/checkout/src/views/Checkout/TokenSelection/index.tsx @@ -124,7 +124,8 @@ export const TokenSelectionContent = () => { selectedCurrency: { address: token.contractAddress, chainId: token.chainId - } + }, + isInitialBalanceChecked: true } } ])