From 69d49bf8a38fb374a5f4550e90cf138f6f51b105 Mon Sep 17 00:00:00 2001 From: jinchung Date: Fri, 14 Jun 2024 20:39:56 -0400 Subject: [PATCH] Revert "swap button states (#5785)" This reverts commit fc0ff0509f1684d7ab3065b4852e36ecf7817e9e. --- .../components/GestureHandlerV1Button.tsx | 4 +- .../Swap/components/SwapActionButton.tsx | 35 ++-- .../Swap/components/SwapBottomPanel.tsx | 24 +-- .../Swap/components/SwapInputAsset.tsx | 8 +- .../Swap/components/SwapOutputAsset.tsx | 17 +- .../screens/Swap/hooks/useEstimatedGasFee.ts | 70 ++++++-- .../SyncSwapStateAndSharedValues.tsx | 109 ------------ .../screens/Swap/providers/swap-provider.tsx | 155 ++++++++---------- .../Swap/resources/assets/userAssets.ts | 3 +- src/languages/en_US.json | 5 +- src/resources/assets/assetSelectors.ts | 6 +- src/resources/assets/useUserAsset.ts | 17 -- src/state/assets/userAssets.ts | 8 +- 13 files changed, 169 insertions(+), 292 deletions(-) delete mode 100644 src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index f4752f619a8..4805dc577bb 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -1,10 +1,10 @@ -import { ButtonPressAnimation } from '@/components/animations'; -import { IS_IOS } from '@/env'; import ConditionalWrap from 'conditional-wrap'; import React from 'react'; import { StyleProp, ViewProps, ViewStyle } from 'react-native'; import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import Animated, { AnimatedStyle, runOnJS, useAnimatedGestureHandler } from 'react-native-reanimated'; +import { ButtonPressAnimation } from '@/components/animations'; +import { IS_IOS } from '@/env'; export type GestureHandlerButtonProps = { buttonPressWrapperStyleIOS?: StyleProp; diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index 6bb346891e4..f447a42eff8 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -1,15 +1,13 @@ /* eslint-disable no-nested-ternary */ import React from 'react'; -import { StyleProp, StyleSheet, TextStyle, ViewProps, ViewStyle } from 'react-native'; -import Animated, { DerivedValue, useAnimatedProps, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; +import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; +import Animated, { DerivedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; +import { AnimatedText, Box, Column, Columns, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { getColorValueForThemeWorklet } from '@/__swaps__/utils/swaps'; -import { AnimatedText, Box, Column, Columns, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { GestureHandlerV1Button } from './GestureHandlerV1Button'; -const AnimatedGestureHandlerV1Button = Animated.createAnimatedComponent(GestureHandlerV1Button); - export const SwapActionButton = ({ asset, borderRadius, @@ -25,8 +23,6 @@ export const SwapActionButton = ({ scaleTo, small, style, - disabled, - isLoading, }: { asset: DerivedValue; borderRadius?: number; @@ -42,8 +38,6 @@ export const SwapActionButton = ({ scaleTo?: number; small?: boolean; style?: ViewStyle; - disabled?: DerivedValue; - isLoading?: DerivedValue; }) => { const { isDarkMode } = useColorMode(); const fallbackColor = useForegroundColor('label'); @@ -86,8 +80,6 @@ export const SwapActionButton = ({ }, shadowOpacity: isDarkMode ? 0.2 : small ? 0.2 : 0.36, shadowRadius: isDarkMode ? 26 : small ? 9 : 15, - // we don't want to change the opacity when it's loading - opacity: !isLoading?.value && disabled?.value ? 0.6 : 1, }; }); @@ -105,20 +97,15 @@ export const SwapActionButton = ({ return rightIcon; }); - const buttonAnimatedProps = useAnimatedProps(() => { - return { - pointerEvents: (disabled?.value ? 'none' : 'box-only') as ViewProps['pointerEvents'], - disableButtonPressWrapper: disabled?.value, - scaleTo: scaleTo || (hugContent ? undefined : 0.925), - }; - }); - return ( - - + ); }; diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index 1d9f7a703b2..48ec3a71371 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; import { PanGestureHandler } from 'react-native-gesture-handler'; -import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated'; import { Box, Column, Columns, Separator, globalColors, useColorMode } from '@/design-system'; import { safeAreaInsetValues } from '@/utils'; @@ -21,8 +21,15 @@ import { SwapActionButton } from './SwapActionButton'; export function SwapBottomPanel() { const { isDarkMode } = useColorMode(); - const { confirmButtonIconStyle, confirmButtonProps, internalSelectedOutputAsset, AnimatedSwapStyles, SwapNavigation, configProgress } = - useSwapContext(); + const { + confirmButtonIcon, + confirmButtonIconStyle, + confirmButtonLabel, + internalSelectedOutputAsset, + AnimatedSwapStyles, + SwapNavigation, + configProgress, + } = useSwapContext(); const { swipeToDismissGestureHandler, gestureY } = useBottomPanelGestureHandler(); @@ -43,10 +50,6 @@ export function SwapBottomPanel() { }; }); - const icon = useDerivedValue(() => confirmButtonProps.value.icon); - const label = useDerivedValue(() => confirmButtonProps.value.label); - const disabled = useDerivedValue(() => confirmButtonProps.value.disabled); - return ( // @ts-expect-error Property 'children' does not exist on type @@ -77,12 +80,11 @@ export function SwapBottomPanel() { diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index f47c8ee3168..f27c8a0ac30 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -1,20 +1,20 @@ import MaskedView from '@react-native-masked-view/masked-view'; import React from 'react'; -import { StatusBar, StyleSheet } from 'react-native'; +import { StyleSheet, StatusBar } from 'react-native'; import Animated, { useDerivedValue } from 'react-native-reanimated'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; import { AnimatedText, Box, Column, Columns, Stack, useColorMode } from '@/design-system'; -import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; -import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button'; import { SwapActionButton } from '@/__swaps__/screens/Swap/components/SwapActionButton'; +import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { SwapInput } from '@/__swaps__/screens/Swap/components/SwapInput'; +import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenList'; import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { IS_ANDROID } from '@/env'; +import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { AnimatedSwapCoinIcon } from './AnimatedSwapCoinIcon'; import * as i18n from '@/languages'; diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 34869a38cad..e639139a9ed 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -1,26 +1,25 @@ -import { AnimatedText, Box, Column, Columns, Stack, useColorMode } from '@/design-system'; import MaskedView from '@react-native-masked-view/masked-view'; import React, { useCallback } from 'react'; -import { StatusBar, StyleSheet } from 'react-native'; +import { StyleSheet, StatusBar } from 'react-native'; import Animated, { runOnJS, useDerivedValue } from 'react-native-reanimated'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; - -import { AnimatedSwapCoinIcon } from '@/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon'; -import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; -import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; +import { AnimatedText, Box, Column, Columns, Stack, useColorMode } from '@/design-system'; import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button'; import { SwapActionButton } from '@/__swaps__/screens/Swap/components/SwapActionButton'; +import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import { SwapInput } from '@/__swaps__/screens/Swap/components/SwapInput'; +import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge'; +import { AnimatedSwapCoinIcon } from '@/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon'; import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenList'; import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/__swaps__/types/chains'; import { IS_ANDROID } from '@/env'; -import * as i18n from '@/languages'; +import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { ethereumUtils } from '@/utils'; +import { ChainId } from '@/__swaps__/types/chains'; +import * as i18n from '@/languages'; const SELECT_LABEL = i18n.t(i18n.l.swap.select); const NO_BALANCE_LABEL = i18n.t(i18n.l.swap.no_balance); diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index 3b08d90bbe5..898a47797ea 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,11 +1,13 @@ +import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; import { ChainId } from '@/__swaps__/types/chains'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, convertAmountToNativeDisplayWorklet, formatNumber, multiply } from '@/__swaps__/utils/numbers'; import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; +import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; +import { useDebouncedCallback } from 'use-debounce'; import { formatUnits } from 'viem'; - -import { useSyncedSwapQuoteStore } from '../providers/SyncSwapStateAndSharedValues'; +import { useSwapContext } from '../providers/swap-provider'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; import { useAccountSettings } from '@/hooks'; @@ -18,11 +20,6 @@ function safeBigInt(value: string) { } } -export function calculateGasFee(gasSettings: GasSettings, gasLimit: string) { - const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; - return multiply(gasLimit, amount); -} - export function useEstimatedGasFee({ chainId, gasLimit, @@ -39,23 +36,66 @@ export function useEstimatedGasFee({ return useMemo(() => { if (!gasLimit || !gasSettings || !nativeNetworkAsset?.price) return; - const fee = calculateGasFee(gasSettings, gasLimit); + const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; + + const totalWei = multiply(gasLimit, amount); const networkAssetPrice = nativeNetworkAsset.price.value?.toString(); - if (!networkAssetPrice) return `${formatNumber(weiToGwei(fee))} Gwei`; + if (!networkAssetPrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; - const feeFormatted = formatUnits(safeBigInt(fee), nativeNetworkAsset.decimals).toString(); - const feeInUserCurrency = multiply(networkAssetPrice, feeFormatted); + const gasAmount = formatUnits(safeBigInt(totalWei), nativeNetworkAsset.decimals).toString(); + const feeInUserCurrency = multiply(networkAssetPrice, gasAmount); return convertAmountToNativeDisplayWorklet(feeInUserCurrency, nativeCurrency); }, [gasLimit, gasSettings, nativeCurrency, nativeNetworkAsset]); } export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { - const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); - const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); + const { internalSelectedInputAsset: assetToSell, internalSelectedOutputAsset: assetToBuy, quote } = useSwapContext(); + + const [state, setState] = useState({ + assetToBuy: assetToBuy.value, + assetToSell: assetToSell.value, + chainId: assetToSell.value?.chainId ?? ChainId.mainnet, + quote: quote.value, + }); + + const debouncedStateSet = useDebouncedCallback(setState, 100, { leading: false, trailing: true }); + + // Updates the state as a single block in response to quote changes to ensure the gas fee is cleanly updated once + useAnimatedReaction( + () => quote.value, + (current, previous) => { + if (!assetToSell.value || !assetToBuy.value || !current || !previous || 'error' in current) return; + + const isSwappingMoreThanAvailableBalance = greaterThanWorklet( + current.sellAmount.toString(), + toScaledIntegerWorklet(assetToSell.value.balance.amount, assetToSell.value.decimals) + ); + + // Skip gas fee recalculation if the user is trying to swap more than their available balance, as it isn't + // needed and was previously resulting in errors in useEstimatedGasFee. + if (isSwappingMoreThanAvailableBalance) return; + + if (current !== previous) { + runOnJS(debouncedStateSet)({ + assetToBuy: assetToBuy.value, + assetToSell: assetToSell.value, + chainId: assetToSell.value?.chainId ?? ChainId.mainnet, + quote: current, + }); + } + } + ); + + const { data: gasLimit, isFetching } = useSwapEstimatedGasLimit( + { chainId: state.chainId, quote: state.quote, assetToSell: state.assetToSell }, + { + enabled: !!state.quote && !!state.assetToSell && !!state.assetToBuy && !('error' in quote), + } + ); - const estimatedFee = useEstimatedGasFee({ chainId, gasLimit: estimatedGasLimit, gasSettings }); + const estimatedFee = useEstimatedGasFee({ chainId: state.chainId, gasLimit, gasSettings }); return useMemo(() => ({ isLoading: isFetching, data: estimatedFee }), [estimatedFee, isFetching]); } diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx deleted file mode 100644 index 3557f0ddb6f..00000000000 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; -import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { add } from '@/__swaps__/utils/numbers'; -import { ParsedAddressAsset } from '@/entities'; -import { lessThan } from '@/helpers/utilities'; -import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; -import { debounce } from 'lodash'; -import { useEffect } from 'react'; -import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; -import { formatUnits } from 'viem'; -import { create } from 'zustand'; -import { calculateGasFee } from '../hooks/useEstimatedGasFee'; -import { useSelectedGas } from '../hooks/useSelectedGas'; -import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; -import { useSwapContext } from './swap-provider'; - -type InternalSyncedSwapState = { - assetToBuy: ExtendedAnimatedAssetWithColors | undefined; - assetToSell: ExtendedAnimatedAssetWithColors | undefined; - chainId: ChainId | undefined; - quote: Quote | CrosschainQuote | QuoteError | null; -}; -export const useSyncedSwapQuoteStore = create(() => ({ - assetToBuy: undefined, - assetToSell: undefined, - chainId: undefined, - quote: null, -})); -const setInternalSyncedSwapStore = debounce((state: InternalSyncedSwapState) => useSyncedSwapQuoteStore.setState(state), 100, { - leading: false, - trailing: true, -}); - -export const SyncQuoteSharedValuesToState = () => { - const { internalSelectedInputAsset: assetToSell, internalSelectedOutputAsset: assetToBuy, quote } = useSwapContext(); - - // Updates the state as a single block in response to quote changes to ensure the gas fee is cleanly updated once - useAnimatedReaction( - () => quote.value, - (current, previous) => { - if (!assetToSell.value || !assetToBuy.value || !current || 'error' in current) return; - - const isSwappingMoreThanAvailableBalance = greaterThanWorklet( - current.sellAmount.toString(), - toScaledIntegerWorklet(assetToSell.value.balance.amount, assetToSell.value.decimals) - ); - - // Skip gas fee recalculation if the user is trying to swap more than their available balance, as it isn't - // needed and was previously resulting in errors in useEstimatedGasFee. - if (isSwappingMoreThanAvailableBalance) return; - - if (!previous || current !== previous) { - runOnJS(setInternalSyncedSwapStore)({ - assetToBuy: assetToBuy.value, - assetToSell: assetToSell.value, - chainId: assetToSell.value?.chainId, - quote: current, - }); - } - } - ); - - return null; -}; - -const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsset: ParsedAddressAsset | undefined) => { - if (!nativeNetworkAsset) return false; - const userBalance = nativeNetworkAsset.balance?.amount || '0'; - - const quoteValue = quote.value?.toString() || '0'; - const totalNativeSpentInTx = formatUnits(BigInt(add(quoteValue, gasFee)), nativeNetworkAsset.decimals); - - return lessThan(totalNativeSpentInTx, userBalance); -}; - -export function SyncGasStateToSharedValues() { - const { hasEnoughFundsForGas, internalSelectedInputAsset, SwapInputController } = useSwapContext(); - - const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); - - const gasSettings = useSelectedGas(chainId); - const { data: userNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); - const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); - - useEffect(() => { - hasEnoughFundsForGas.value = false; - if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote) return; - - const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - hasEnoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); - - return () => { - hasEnoughFundsForGas.value = false; - }; - }, [ - estimatedGasLimit, - gasSettings, - hasEnoughFundsForGas, - quote, - internalSelectedInputAsset.value?.balance.amount, - SwapInputController.inputValues.value.inputAmount, - userNativeNetworkAsset, - isFetching, - ]); - - return null; -} diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 7dddad7542c..e261abba5c8 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -1,6 +1,6 @@ // @refresh import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef } from 'react'; -import { InteractionManager, NativeModules, StyleProp, TextInput, TextStyle } from 'react-native'; +import { StyleProp, TextStyle, TextInput, NativeModules, InteractionManager } from 'react-native'; import { AnimatedRef, DerivedValue, @@ -13,43 +13,41 @@ import { useSharedValue, } from 'react-native-reanimated'; +import * as i18n from '@/languages'; +import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; import { INITIAL_SLIDER_POSITION, SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { useAnimatedSwapStyles } from '@/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles'; -import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController'; -import { NavigationSteps, useSwapNavigation } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; -import { useSwapSettings } from '@/__swaps__/screens/Swap/hooks/useSwapSettings'; import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; -import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; -import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; +import { useSwapNavigation, NavigationSteps } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; +import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController'; import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; +import { useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; +import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore'; import { parseAssetAndExtend } from '@/__swaps__/utils/swaps'; -import { getFlashbotsProvider, getProviderForNetwork, isHardHat } from '@/handlers/web3'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { useAccountSettings } from '@/hooks'; -import * as i18n from '@/languages'; +import { ChainId } from '@/__swaps__/types/chains'; import { RainbowError, logger } from '@/logger'; -import { loadWallet } from '@/model/wallet'; +import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { Navigation } from '@/navigation'; +import { WrappedAlert as Alert } from '@/helpers/alert'; import Routes from '@/navigation/routesNames'; +import { ethereumUtils } from '@/utils'; +import { getFlashbotsProvider, getProviderForNetwork, isHardHat } from '@/handlers/web3'; +import { loadWallet } from '@/model/wallet'; import { walletExecuteRap } from '@/raps/execute'; -import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; +import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { swapsStore } from '@/state/swaps/swapsStore'; -import { ethereumUtils } from '@/utils'; -import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; - -import { equalWorklet, lessThanOrEqualToWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath'; -import { analyticsV2 } from '@/analytics'; +import { useAccountSettings } from '@/hooks'; +import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; +import { equalWorklet } from '@/__swaps__/safe-math/SafeMath'; +import { useSwapSettings } from '../hooks/useSwapSettings'; +import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; import { getNetworkObj } from '@/networks'; import { userAssetsStore } from '@/state/assets/userAssets'; +import { analyticsV2 } from '@/analytics'; import { Address } from 'viem'; -import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; -import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisabled'; -import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const tapToSwap = i18n.t(i18n.l.swap.actions.tap_to_swap); @@ -57,9 +55,6 @@ const save = i18n.t(i18n.l.swap.actions.save); const enterAmount = i18n.t(i18n.l.swap.actions.enter_amount); const review = i18n.t(i18n.l.swap.actions.review); const fetchingPrices = i18n.t(i18n.l.swap.actions.fetching_prices); -const selectToken = i18n.t(i18n.l.swap.actions.select_token); -const errorLabel = i18n.t(i18n.l.swap.actions.error); -const insufficientFunds = i18n.t(i18n.l.swap.actions.insufficient_funds); interface SwapContextType { isFetching: SharedValue; @@ -104,16 +99,9 @@ interface SwapContextType { SwapNavigation: ReturnType; SwapWarning: ReturnType; - confirmButtonProps: Readonly< - SharedValue<{ - label: string; - icon?: string; - disabled?: boolean; - }> - >; + confirmButtonIcon: Readonly>; + confirmButtonLabel: Readonly>; confirmButtonIconStyle: StyleProp; - - hasEnoughFundsForGas: SharedValue; } const SwapContext = createContext(undefined); @@ -442,7 +430,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; runOnUI(updateChainId)(chainId); - swapsStore.setState({ selectedOutputChainId: chainId }); + useSwapsStore.setState({ selectedOutputChainId: chainId }); }; const updateAssetValue = useCallback( @@ -514,19 +502,19 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { : asset; if (isSameAsOtherAsset) { - swapsStore.setState({ + useSwapsStore.setState({ [type === SwapAssetType.inputAsset ? SwapAssetType.outputAsset : SwapAssetType.inputAsset]: flippedAssetOrNull, [type]: otherSelectedAsset, }); } else { - swapsStore.setState({ [type]: assetToSet }); + useSwapsStore.setState({ [type]: assetToSet }); } } else { SwapInputController.quoteFetchingInterval.start(); } const shouldUpdateSelectedOutputChainId = - type === SwapAssetType.inputAsset && swapsStore.getState().selectedOutputChainId !== extendedAsset?.chainId; + type === SwapAssetType.inputAsset && useSwapsStore.getState().selectedOutputChainId !== extendedAsset?.chainId; const shouldUpdateAnimatedSelectedOutputChainId = type === SwapAssetType.inputAsset && selectedOutputChainId.value !== extendedAsset?.chainId; @@ -540,7 +528,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { chainSetTimeoutId.current = setTimeout(() => { InteractionManager.runAfterInteractions(() => { if (shouldUpdateSelectedOutputChainId) { - swapsStore.setState({ + useSwapsStore.setState({ selectedOutputChainId: extendedAsset?.chainId ?? ChainId.mainnet, }); } @@ -578,69 +566,63 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const hasEnoughFundsForGas = useSharedValue(false); - - const confirmButtonProps = useDerivedValue(() => { + const confirmButtonIcon = useDerivedValue(() => { if (isSwapping.value) { - return { label: swapping, disabled: true }; + return ''; } if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return { icon: '􀎽', label: tapToSwap, disabled: false }; - } - - if (configProgress.value === NavigationSteps.SHOW_GAS) { - return { icon: '􀆅', label: save, disabled: false }; - } - - const hasSelectedAssets = internalSelectedInputAsset.value && internalSelectedOutputAsset.value; - if (!hasSelectedAssets) { - return { label: selectToken, disabled: true }; + return '􀎽'; + } else if (configProgress.value === NavigationSteps.SHOW_GAS) { + return '􀆅'; } - if (isFetching.value) { - return { label: fetchingPrices, disabled: true }; + if (isQuoteStale.value === 1 && sliderPressProgress.value === 0) { + return ''; } const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); - const isQuoteError = quote.value && 'error' in quote.value; - if (isQuoteError) { - return { label: errorLabel, disabled: true }; + if ( + (isInputZero && isOutputZero) || + isFetching.value || + (SwapInputController.inputMethod.value === 'slider' && SwapInputController.percentageToSwap.value === 0) + ) { + return ''; + } else { + return '􀕹'; } + }); - if (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) { - return { label: enterAmount, disabled: true }; + const confirmButtonLabel = useDerivedValue(() => { + if (isSwapping.value) { + return swapping; } - const inputAsset = internalSelectedInputAsset.value; - const sellAmount = (() => { - const inputAmount = SwapInputController.inputValues.value.inputAmount; - if (!quote.value || 'error' in quote.value) return inputAmount; - return quote.value.sellAmount.toString(); - })(); - - const enoughFundsForSwap = - inputAsset && lessThanOrEqualToWorklet(sellAmount, toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals)); - - if (!isFetching && (!hasEnoughFundsForGas.value || !enoughFundsForSwap)) { - return { label: insufficientFunds, disabled: true }; + if (configProgress.value === NavigationSteps.SHOW_REVIEW) { + return tapToSwap; + } else if (configProgress.value === NavigationSteps.SHOW_GAS) { + return save; } - if ( - [SwapWarningType.no_quote_available, SwapWarningType.no_route_found, SwapWarningType.insufficient_liquidity].includes( - SwapWarning.swapWarning.value.type - ) - ) { - return { icon: '􀕹', label: review, disabled: true }; + if (isFetching.value || (isQuoteStale.value === 1 && SwapInputController.inputMethod.value !== 'slider')) { + return fetchingPrices; } - if (isQuoteError) { - return { label: errorLabel, disabled: true }; - } + const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); + const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); - return { icon: '􀕹', label: review, disabled: false }; + if (SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value) { + return enterAmount; + } else if ( + SwapInputController.inputMethod.value === 'slider' && + (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) + ) { + return enterAmount; + } else { + return review; + } }); const confirmButtonIconStyle = useAnimatedStyle(() => { @@ -699,15 +681,12 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { SwapNavigation, SwapWarning, - confirmButtonProps, + confirmButtonIcon, + confirmButtonLabel, confirmButtonIconStyle, - - hasEnoughFundsForGas, }} > {children} - - ); }; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 37423b6d930..024392c1969 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { ADDYS_API_KEY } from 'react-native-dotenv'; import { Address } from 'viem'; - +import { ADDYS_API_KEY } from 'react-native-dotenv'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { getIsHardhatConnected } from '@/handlers/web3'; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 41d4841ed83..db5c8c01884 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1939,10 +1939,7 @@ "enter_amount": "Enter Amount", "review": "Review", "fetching_prices": "Fetching", - "swapping": "Swapping", - "select_token": "Select Token", - "insufficient_funds": "Insufficient Funds", - "error": "Error" + "swapping": "Swapping" }, "aggregators": { "rainbow": "Rainbow" diff --git a/src/resources/assets/assetSelectors.ts b/src/resources/assets/assetSelectors.ts index c5fb9bf467b..db8a15c9c92 100644 --- a/src/resources/assets/assetSelectors.ts +++ b/src/resources/assets/assetSelectors.ts @@ -1,8 +1,8 @@ -import { ParsedAddressAsset } from '@/entities'; -import { parseAssetsNative } from '@/parsers'; +import { RainbowAddressAssets } from './types'; import isEmpty from 'lodash/isEmpty'; import isNil from 'lodash/isNil'; -import { RainbowAddressAssets } from './types'; +import { ParsedAddressAsset } from '@/entities'; +import { parseAssetsNative } from '@/parsers'; const EMPTY_ARRAY: any = []; diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index 7a61c9f2558..96c5b7a6b34 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,11 +1,7 @@ -import { ChainId } from '@/__swaps__/types/chains'; -import { Network } from '@/helpers'; import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; -import { getNetworkObj } from '@/networks'; import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; export function useUserAsset(uniqueId: string) { const { accountAddress, nativeCurrency } = useAccountSettings(); @@ -22,16 +18,3 @@ export function useUserAsset(uniqueId: string) { } ); } - -export const getNetworkNativeAssetUniqueId = (chainId: ChainId) => { - const network = getNetworkFromChainId(chainId); - const { nativeCurrency } = getNetworkObj(network); - const { mainnetAddress, address } = nativeCurrency; - const uniqueId = mainnetAddress ? `${mainnetAddress}_${Network.mainnet}` : `${address}_${network}`; - return uniqueId; -}; - -export function useUserNativeNetworkAsset(chainId: ChainId) { - const uniqueId = getNetworkNativeAssetUniqueId(chainId); - return useUserAsset(uniqueId); -} diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index f1cd93e5c6e..59ba39f4162 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,10 +1,10 @@ -import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; -import { SUPPORTED_CHAIN_IDS } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; -import { Address } from 'viem'; import store from '@/redux/store'; +import { SUPPORTED_CHAIN_IDS } from '@/references'; +import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; const SEARCH_CACHE_MAX_ENTRIES = 50; const SMALL_BALANCE_THRESHOLD = store.getState().settings.nativeCurrency === 'ETH' ? 0.000005 : 0.02;