From ba5a7032daa363cde84ccaf4d2c97ace359000bc Mon Sep 17 00:00:00 2001 From: samuelea Date: Tue, 14 Oct 2025 15:52:23 -0400 Subject: [PATCH 1/4] update navigation to include information related to first visit --- .../SequenceCheckoutProvider.tsx | 4 +++- .../checkout/src/contexts/NavigationCheckout.ts | 1 + .../PaymentMethodSelect/PayWithCrypto/index.tsx | 16 ++++++++++++++++ .../src/views/Checkout/TokenSelection/index.tsx | 3 ++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx index 10d4c008f..46a828255 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: { + isFirstVisit: true + } } } export const SequenceCheckoutProvider = ({ children, config }: SequenceCheckoutProviderProps) => { diff --git a/packages/checkout/src/contexts/NavigationCheckout.ts b/packages/checkout/src/contexts/NavigationCheckout.ts index d9d6d5ba3..0c6eab6a0 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 } + isFirstVisit: 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..e42b6480d 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx @@ -24,6 +24,7 @@ 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' @@ -46,6 +47,21 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P const [isError, setIsError] = useState(false) const { navigation, setNavigation } = useNavigationCheckout() + const isFirstVisit = (navigation.params as PaymentMethodSelectionParams).isFirstVisit + console.log('isFirstVisit', isFirstVisit) + + useEffect(() => { + setTimeout(() => { + setNavigation({ + location: 'payment-method-selection', + params: { + ...navigation.params, + isFirstVisit: false + } + }) + }, 5000) + }, []) + const { chain, collectibles, diff --git a/packages/checkout/src/views/Checkout/TokenSelection/index.tsx b/packages/checkout/src/views/Checkout/TokenSelection/index.tsx index f8dc2984f..ea1e6cb16 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 - } + }, + isFirstVisit: false } } ]) From 4e2eafb4151372ef452031c32052ff9c30caf627 Mon Sep 17 00:00:00 2001 From: samuelea Date: Tue, 14 Oct 2025 21:02:09 -0400 Subject: [PATCH 2/4] initial check logic --- .../SequenceCheckoutProvider.tsx | 2 +- .../src/contexts/NavigationCheckout.ts | 2 +- .../PayWithCrypto/index.tsx | 51 +++++++++++++------ .../views/Checkout/TokenSelection/index.tsx | 2 +- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx index 46a828255..4f1f67e65 100644 --- a/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx +++ b/packages/checkout/src/components/SequenceCheckoutProvider/SequenceCheckoutProvider.tsx @@ -57,7 +57,7 @@ const getDefaultLocationCheckout = (): NavigationCheckout => { return { location: 'payment-method-selection', params: { - isFirstVisit: true + isInitialBalanceChecked: false } } } diff --git a/packages/checkout/src/contexts/NavigationCheckout.ts b/packages/checkout/src/contexts/NavigationCheckout.ts index 0c6eab6a0..0b485d1ec 100644 --- a/packages/checkout/src/contexts/NavigationCheckout.ts +++ b/packages/checkout/src/contexts/NavigationCheckout.ts @@ -7,7 +7,7 @@ export interface PaymentMethodSelectionParams { address: string chainId: number } - isFirstVisit: boolean + 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 e42b6480d..9cf008892 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx @@ -47,21 +47,6 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P const [isError, setIsError] = useState(false) const { navigation, setNavigation } = useNavigationCheckout() - const isFirstVisit = (navigation.params as PaymentMethodSelectionParams).isFirstVisit - console.log('isFirstVisit', isFirstVisit) - - useEffect(() => { - setTimeout(() => { - setNavigation({ - location: 'payment-method-selection', - params: { - ...navigation.params, - isFirstVisit: false - } - }) - }, 5000) - }, []) - const { chain, collectibles, @@ -185,13 +170,16 @@ 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) @@ -200,6 +188,37 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P const isInsufficientBalance = tokenBalance === undefined || (tokenBalance?.balance && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) + const findSwapQuote = async () => { + // find new swap quote logic here + // useFindSwapRoutes: fromTokens[x].price => raw value for the full transactions + // get all balances from the swap routes and comprate each + + // is swap quote found update with new selected currency, otherwise simply update balance check flag + setNavigation({ + location: 'payment-method-selection', + params: { + ...navigation.params, + isInitialBalanceChecked: true + } + }) + } + + useEffect(() => { + if (!isInitialBalanceChecked && !tokenBalancesIsLoading) { + if (isInsufficientBalance) { + findSwapQuote() + } else { + setNavigation({ + location: 'payment-method-selection', + params: { + ...navigation.params, + isInitialBalanceChecked: true + } + }) + } + } + }, [isInitialBalanceChecked, tokenBalancesIsLoading]) + const isApproved: boolean = (allowanceData as bigint) >= BigInt(price) || isNativeToken const formattedPrice = formatUnits(BigInt(selectedCurrencyPrice), selectedCurrencyInfo?.decimals || 0) diff --git a/packages/checkout/src/views/Checkout/TokenSelection/index.tsx b/packages/checkout/src/views/Checkout/TokenSelection/index.tsx index ea1e6cb16..ee80a9fab 100644 --- a/packages/checkout/src/views/Checkout/TokenSelection/index.tsx +++ b/packages/checkout/src/views/Checkout/TokenSelection/index.tsx @@ -125,7 +125,7 @@ export const TokenSelectionContent = () => { address: token.contractAddress, chainId: token.chainId }, - isFirstVisit: false + isInitialBalanceChecked: true } } ]) From 74056500319aaa14de856851f70170e1b898db5a Mon Sep 17 00:00:00 2001 From: samuelea Date: Thu, 16 Oct 2025 17:53:45 -0400 Subject: [PATCH 3/4] added inital balacne check --- .../PayWithCrypto/index.tsx | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx index 9cf008892..cc5204794 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx @@ -13,6 +13,7 @@ import { useGetCoinPrices, useGetContractInfo, useGetSwapQuote, + useGetSwapRoutes, useGetTokenBalancesSummary, useIndexerClient } from '@0xsequence/hooks' @@ -157,7 +158,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, @@ -186,25 +188,78 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P ) const isInsufficientBalance = - tokenBalance === undefined || (tokenBalance?.balance && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) + tokenBalance === undefined || + (tokenBalance?.balance && tokenBalance.balance !== '' && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) + + 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 () => { - // find new swap quote logic here - // useFindSwapRoutes: fromTokens[x].price => raw value for the full transactions - // get all balances from the swap routes and comprate each + let validSwapRoute: string | undefined + + for (let i = 0; i < swapRoutes.length; i++) { + 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) + ) + + console.log('balance', balance) + console.log('fromToken', fromToken) + if (!balance) { + continue + } + if (BigInt(balance.balance || '0') >= BigInt(fromToken.price || '0')) { + validSwapRoute = fromToken.address + break + } + } + } - // is swap quote found update with new selected currency, otherwise simply update balance check flag setNavigation({ location: 'payment-method-selection', params: { ...navigation.params, + selectedCurrency: { + address: validSwapRoute || selectedCurrency.address, + chainId: chainId + }, isInitialBalanceChecked: true } }) } useEffect(() => { - if (!isInitialBalanceChecked && !tokenBalancesIsLoading) { + if (!isInitialBalanceChecked && !tokenBalancesIsLoading && !swapRoutesIsLoading && !swapRoutesTokenBalancesIsLoading) { if (isInsufficientBalance) { findSwapQuote() } else { @@ -217,7 +272,13 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P }) } } - }, [isInitialBalanceChecked, tokenBalancesIsLoading]) + }, [ + isInitialBalanceChecked, + isInsufficientBalance, + tokenBalancesIsLoading, + swapRoutesIsLoading, + swapRoutesTokenBalancesIsLoading + ]) const isApproved: boolean = (allowanceData as bigint) >= BigInt(price) || isNativeToken From e3a9e4ed3839bce8320a50df0b738d4632475b8d Mon Sep 17 00:00:00 2001 From: samuelea Date: Thu, 16 Oct 2025 18:16:20 -0400 Subject: [PATCH 4/4] moved auto select to its own hook --- .../PayWithCrypto/index.tsx | 99 ++------------- .../PayWithCrypto/useInitialBalanceCheck.tsx | 117 ++++++++++++++++++ 2 files changed, 127 insertions(+), 89 deletions(-) create mode 100644 packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/useInitialBalanceCheck.tsx diff --git a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx index cc5204794..b0deae4ab 100644 --- a/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx +++ b/packages/checkout/src/views/Checkout/PaymentMethodSelect/PayWithCrypto/index.tsx @@ -13,7 +13,6 @@ import { useGetCoinPrices, useGetContractInfo, useGetSwapQuote, - useGetSwapRoutes, useGetTokenBalancesSummary, useIndexerClient } from '@0xsequence/hooks' @@ -31,6 +30,8 @@ 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 @@ -191,94 +192,14 @@ export const PayWithCryptoTab = ({ skipOnCloseCallback, isSwitchingChainRef }: P tokenBalance === undefined || (tokenBalance?.balance && tokenBalance.balance !== '' && BigInt(tokenBalance.balance) < BigInt(selectedCurrencyPrice)) - 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 - - for (let i = 0; i < swapRoutes.length; i++) { - 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) - ) - - console.log('balance', balance) - console.log('fromToken', fromToken) - 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 || selectedCurrency.address, - 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 - ]) + 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 + ]) +}