From fc0ff0509f1684d7ab3065b4852e36ecf7817e9e Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 14 Jun 2024 17:41:37 -0300 Subject: [PATCH 1/4] swap button states (#5785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * fix gesture button states * Safemath pt 2 (#5778) * add more fns * accept string or number * update errors * fix dynamic island overlap on recieve modal (#5672) * . * oop * oop * okay ty ben * change background opacity to 1 * . * oop * . * Gas optimizations (#5779) * perf * ✨ * useWhyDidYouUpdate * EstimatedSwapGasFee * keepPreviousData * AnimatedText * isSameAddress * fix other networks section (#5784) * Swaps: fix favorite button press (#5782) * fix * android fix * todo * remove console logs * Insufficient Funds * remove todo * move cache getter closer to fetcher implentation * fix * :) * 🍕 * or equal 🤌 * remove unused isSameAddress util * just reordering declarations * error i18n * useGasSharedValues * remove estimating * fix label flickering * fix review panel not prompting * Revert "Lint on pre-commit (#5836)" This reverts commit d56ed46e7772cd51e54a0f8214947de01e48dd47. * fix a bunch of shit --------- Co-authored-by: Matthew Wall Co-authored-by: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Co-authored-by: brdy <41711440+BrodyHughes@users.noreply.github.com> Co-authored-by: Ben Goldberg --- .../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, 292 insertions(+), 169 deletions(-) create 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 4805dc577bb..f4752f619a8 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 f447a42eff8..6bb346891e4 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -1,13 +1,15 @@ /* eslint-disable no-nested-ternary */ import React from 'react'; -import { StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native'; -import Animated, { DerivedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'; +import { StyleProp, StyleSheet, TextStyle, ViewProps, ViewStyle } from 'react-native'; +import Animated, { DerivedValue, useAnimatedProps, 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, @@ -23,6 +25,8 @@ export const SwapActionButton = ({ scaleTo, small, style, + disabled, + isLoading, }: { asset: DerivedValue; borderRadius?: number; @@ -38,6 +42,8 @@ export const SwapActionButton = ({ scaleTo?: number; small?: boolean; style?: ViewStyle; + disabled?: DerivedValue; + isLoading?: DerivedValue; }) => { const { isDarkMode } = useColorMode(); const fallbackColor = useForegroundColor('label'); @@ -80,6 +86,8 @@ 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, }; }); @@ -97,15 +105,20 @@ 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 48ec3a71371..1d9f7a703b2 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, withSpring } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from 'react-native-reanimated'; import { Box, Column, Columns, Separator, globalColors, useColorMode } from '@/design-system'; import { safeAreaInsetValues } from '@/utils'; @@ -21,15 +21,8 @@ import { SwapActionButton } from './SwapActionButton'; export function SwapBottomPanel() { const { isDarkMode } = useColorMode(); - const { - confirmButtonIcon, - confirmButtonIconStyle, - confirmButtonLabel, - internalSelectedOutputAsset, - AnimatedSwapStyles, - SwapNavigation, - configProgress, - } = useSwapContext(); + const { confirmButtonIconStyle, confirmButtonProps, internalSelectedOutputAsset, AnimatedSwapStyles, SwapNavigation, configProgress } = + useSwapContext(); const { swipeToDismissGestureHandler, gestureY } = useBottomPanelGestureHandler(); @@ -50,6 +43,10 @@ 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 @@ -80,11 +77,12 @@ export function SwapBottomPanel() { diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index f27c8a0ac30..f47c8ee3168 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 { StyleSheet, StatusBar } from 'react-native'; +import { StatusBar, StyleSheet } 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 { IS_ANDROID } from '@/env'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; +import { IS_ANDROID } from '@/env'; 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 e639139a9ed..34869a38cad 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -1,25 +1,26 @@ +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 { StyleSheet, StatusBar } from 'react-native'; +import { StatusBar, StyleSheet } from 'react-native'; import Animated, { runOnJS, useDerivedValue } from 'react-native-reanimated'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; -import { AnimatedText, Box, Column, Columns, Stack, useColorMode } from '@/design-system'; + +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 { 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 { IS_ANDROID } from '@/env'; 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 { 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 898a47797ea..3b08d90bbe5 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,13 +1,11 @@ -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, useState } from 'react'; -import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; -import { useDebouncedCallback } from 'use-debounce'; +import { useMemo } from 'react'; import { formatUnits } from 'viem'; -import { useSwapContext } from '../providers/swap-provider'; + +import { useSyncedSwapQuoteStore } from '../providers/SyncSwapStateAndSharedValues'; import { GasSettings } from './useCustomGas'; import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit'; import { useAccountSettings } from '@/hooks'; @@ -20,6 +18,11 @@ 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, @@ -36,66 +39,23 @@ export function useEstimatedGasFee({ return useMemo(() => { if (!gasLimit || !gasSettings || !nativeNetworkAsset?.price) return; - const amount = gasSettings.isEIP1559 ? add(gasSettings.maxBaseFee, gasSettings.maxPriorityFee) : gasSettings.gasPrice; - - const totalWei = multiply(gasLimit, amount); + const fee = calculateGasFee(gasSettings, gasLimit); const networkAssetPrice = nativeNetworkAsset.price.value?.toString(); - if (!networkAssetPrice) return `${formatNumber(weiToGwei(totalWei))} Gwei`; + if (!networkAssetPrice) return `${formatNumber(weiToGwei(fee))} Gwei`; - const gasAmount = formatUnits(safeBigInt(totalWei), nativeNetworkAsset.decimals).toString(); - const feeInUserCurrency = multiply(networkAssetPrice, gasAmount); + const feeFormatted = formatUnits(safeBigInt(fee), nativeNetworkAsset.decimals).toString(); + const feeInUserCurrency = multiply(networkAssetPrice, feeFormatted); return convertAmountToNativeDisplayWorklet(feeInUserCurrency, nativeCurrency); }, [gasLimit, gasSettings, nativeCurrency, nativeNetworkAsset]); } export function useSwapEstimatedGasFee(gasSettings: GasSettings | undefined) { - 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 { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); + const { data: estimatedGasLimit, isFetching } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); - const estimatedFee = useEstimatedGasFee({ chainId: state.chainId, gasLimit, gasSettings }); + const estimatedFee = useEstimatedGasFee({ chainId, gasLimit: estimatedGasLimit, 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 new file mode 100644 index 00000000000..3557f0ddb6f --- /dev/null +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -0,0 +1,109 @@ +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 e261abba5c8..7dddad7542c 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 { StyleProp, TextStyle, TextInput, NativeModules, InteractionManager } from 'react-native'; +import { InteractionManager, NativeModules, StyleProp, TextInput, TextStyle } from 'react-native'; import { AnimatedRef, DerivedValue, @@ -13,41 +13,43 @@ 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 { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; -import { useSwapNavigation, NavigationSteps } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation'; 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 { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -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 { ChainId } from '@/__swaps__/types/chains'; +import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; +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 { RainbowError, logger } from '@/logger'; -import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; +import { loadWallet } from '@/model/wallet'; 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 { useAccountSettings } from '@/hooks'; -import { getGasSettingsBySpeed, getSelectedGas, getSelectedGasSpeed } from '../hooks/useSelectedGas'; +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 { 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); @@ -55,6 +57,9 @@ 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; @@ -99,9 +104,16 @@ interface SwapContextType { SwapNavigation: ReturnType; SwapWarning: ReturnType; - confirmButtonIcon: Readonly>; - confirmButtonLabel: Readonly>; + confirmButtonProps: Readonly< + SharedValue<{ + label: string; + icon?: string; + disabled?: boolean; + }> + >; confirmButtonIconStyle: StyleProp; + + hasEnoughFundsForGas: SharedValue; } const SwapContext = createContext(undefined); @@ -430,7 +442,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; runOnUI(updateChainId)(chainId); - useSwapsStore.setState({ selectedOutputChainId: chainId }); + swapsStore.setState({ selectedOutputChainId: chainId }); }; const updateAssetValue = useCallback( @@ -502,19 +514,19 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { : asset; if (isSameAsOtherAsset) { - useSwapsStore.setState({ + swapsStore.setState({ [type === SwapAssetType.inputAsset ? SwapAssetType.outputAsset : SwapAssetType.inputAsset]: flippedAssetOrNull, [type]: otherSelectedAsset, }); } else { - useSwapsStore.setState({ [type]: assetToSet }); + swapsStore.setState({ [type]: assetToSet }); } } else { SwapInputController.quoteFetchingInterval.start(); } const shouldUpdateSelectedOutputChainId = - type === SwapAssetType.inputAsset && useSwapsStore.getState().selectedOutputChainId !== extendedAsset?.chainId; + type === SwapAssetType.inputAsset && swapsStore.getState().selectedOutputChainId !== extendedAsset?.chainId; const shouldUpdateAnimatedSelectedOutputChainId = type === SwapAssetType.inputAsset && selectedOutputChainId.value !== extendedAsset?.chainId; @@ -528,7 +540,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { chainSetTimeoutId.current = setTimeout(() => { InteractionManager.runAfterInteractions(() => { if (shouldUpdateSelectedOutputChainId) { - useSwapsStore.setState({ + swapsStore.setState({ selectedOutputChainId: extendedAsset?.chainId ?? ChainId.mainnet, }); } @@ -566,63 +578,69 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const confirmButtonIcon = useDerivedValue(() => { + const hasEnoughFundsForGas = useSharedValue(false); + + const confirmButtonProps = useDerivedValue(() => { if (isSwapping.value) { - return ''; + return { label: swapping, disabled: true }; } if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return '􀎽'; - } else if (configProgress.value === NavigationSteps.SHOW_GAS) { - return '􀆅'; + 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 }; } - if (isQuoteStale.value === 1 && sliderPressProgress.value === 0) { - return ''; + if (isFetching.value) { + return { label: fetchingPrices, disabled: true }; } const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); - if ( - (isInputZero && isOutputZero) || - isFetching.value || - (SwapInputController.inputMethod.value === 'slider' && SwapInputController.percentageToSwap.value === 0) - ) { - return ''; - } else { - return '􀕹'; + const isQuoteError = quote.value && 'error' in quote.value; + if (isQuoteError) { + return { label: errorLabel, disabled: true }; } - }); - const confirmButtonLabel = useDerivedValue(() => { - if (isSwapping.value) { - return swapping; + if (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) { + return { label: enterAmount, disabled: true }; } - if (configProgress.value === NavigationSteps.SHOW_REVIEW) { - return tapToSwap; - } else if (configProgress.value === NavigationSteps.SHOW_GAS) { - return save; - } + 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(); + })(); - if (isFetching.value || (isQuoteStale.value === 1 && SwapInputController.inputMethod.value !== 'slider')) { - return fetchingPrices; - } + const enoughFundsForSwap = + inputAsset && lessThanOrEqualToWorklet(sellAmount, toScaledIntegerWorklet(inputAsset.balance.amount, inputAsset.decimals)); - const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); - const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); + if (!isFetching && (!hasEnoughFundsForGas.value || !enoughFundsForSwap)) { + return { label: insufficientFunds, disabled: true }; + } - if (SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value) { - return enterAmount; - } else if ( - SwapInputController.inputMethod.value === 'slider' && - (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero) + if ( + [SwapWarningType.no_quote_available, SwapWarningType.no_route_found, SwapWarningType.insufficient_liquidity].includes( + SwapWarning.swapWarning.value.type + ) ) { - return enterAmount; - } else { - return review; + return { icon: '􀕹', label: review, disabled: true }; + } + + if (isQuoteError) { + return { label: errorLabel, disabled: true }; } + + return { icon: '􀕹', label: review, disabled: false }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { @@ -681,12 +699,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { SwapNavigation, SwapWarning, - confirmButtonIcon, - confirmButtonLabel, + confirmButtonProps, confirmButtonIconStyle, + + hasEnoughFundsForGas, }} > {children} + + ); }; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 024392c1969..37423b6d930 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { Address } from 'viem'; import { ADDYS_API_KEY } from 'react-native-dotenv'; +import { Address } from 'viem'; + 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 db5c8c01884..41d4841ed83 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1939,7 +1939,10 @@ "enter_amount": "Enter Amount", "review": "Review", "fetching_prices": "Fetching", - "swapping": "Swapping" + "swapping": "Swapping", + "select_token": "Select Token", + "insufficient_funds": "Insufficient Funds", + "error": "Error" }, "aggregators": { "rainbow": "Rainbow" diff --git a/src/resources/assets/assetSelectors.ts b/src/resources/assets/assetSelectors.ts index db8a15c9c92..c5fb9bf467b 100644 --- a/src/resources/assets/assetSelectors.ts +++ b/src/resources/assets/assetSelectors.ts @@ -1,8 +1,8 @@ -import { RainbowAddressAssets } from './types'; -import isEmpty from 'lodash/isEmpty'; -import isNil from 'lodash/isNil'; import { ParsedAddressAsset } from '@/entities'; import { parseAssetsNative } from '@/parsers'; +import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; +import { RainbowAddressAssets } from './types'; const EMPTY_ARRAY: any = []; diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index 96c5b7a6b34..7a61c9f2558 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,7 +1,11 @@ +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(); @@ -18,3 +22,16 @@ 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 59ba39f4162..f1cd93e5c6e 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,10 +1,10 @@ -import { Address } from 'viem'; +import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; 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; From 208856b9d92db0f5346c02284603dd3afd108b1d Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Fri, 14 Jun 2024 18:29:25 -0400 Subject: [PATCH 2/4] Add TextShadow component (#5851) * Add TextShadow component, add env var to silence design system emoji warnings * Temporarily default to disabling Android text shadows --- globals.d.ts | 1 + .../components/Heading/Heading.tsx | 8 ++- src/design-system/components/Text/Text.tsx | 8 ++- .../components/TextIcon/TextIcon.tsx | 2 +- .../components/TextShadow/TextShadow.tsx | 62 +++++++++++++++++++ src/design-system/index.ts | 2 + 6 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/design-system/components/TextShadow/TextShadow.tsx diff --git a/globals.d.ts b/globals.d.ts index 398a10deace..4a1cf40b077 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -102,4 +102,5 @@ declare module 'react-native-dotenv' { export const RPC_PROXY_API_KEY_DEV: string; export const REACT_NATIVE_RUDDERSTACK_WRITE_KEY: string; export const RUDDERSTACK_DATA_PLANE_URL: string; + export const SILENCE_EMOJI_WARNINGS: boolean; } diff --git a/src/design-system/components/Heading/Heading.tsx b/src/design-system/components/Heading/Heading.tsx index 304c34cfb6b..70812fd25b0 100644 --- a/src/design-system/components/Heading/Heading.tsx +++ b/src/design-system/components/Heading/Heading.tsx @@ -1,6 +1,7 @@ import React, { ElementRef, forwardRef, ReactNode, useEffect, useMemo } from 'react'; import { Text as NativeText } from 'react-native'; - +import { SILENCE_EMOJI_WARNINGS } from 'react-native-dotenv'; +import { IS_DEV, IS_IOS } from '@/env'; import { CustomColor } from '../../color/useForegroundColor'; import { createLineHeightFixNode } from '../../typography/createLineHeightFixNode'; import { nodeHasEmoji, nodeIsString, renderStringWithEmoji } from '../../typography/renderStringWithEmoji'; @@ -29,7 +30,7 @@ export const Heading = forwardRef, HeadingProps>(f ref ) { useEffect(() => { - if (__DEV__) { + if (IS_DEV && !SILENCE_EMOJI_WARNINGS) { if (!containsEmojiProp && nodeHasEmoji(children)) { // eslint-disable-next-line no-console console.log( @@ -42,6 +43,7 @@ export const Heading = forwardRef, HeadingProps>(f ); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const headingStyle = useHeadingStyle({ align, color, size, weight }); @@ -50,7 +52,7 @@ export const Heading = forwardRef, HeadingProps>(f return ( - {ios && containsEmojiProp && nodeIsString(children) ? renderStringWithEmoji(children) : children} + {IS_IOS && containsEmojiProp && nodeIsString(children) ? renderStringWithEmoji(children) : children} {lineHeightFixNode} ); diff --git a/src/design-system/components/Text/Text.tsx b/src/design-system/components/Text/Text.tsx index 3f3cf3a8314..414fde217e2 100644 --- a/src/design-system/components/Text/Text.tsx +++ b/src/design-system/components/Text/Text.tsx @@ -1,5 +1,7 @@ import React, { ElementRef, forwardRef, ReactNode, useMemo, useEffect } from 'react'; import { Text as NativeText, StyleProp, TextStyle } from 'react-native'; +import { SILENCE_EMOJI_WARNINGS } from 'react-native-dotenv'; +import { IS_DEV, IS_IOS } from '@/env'; import { TextColor } from '../../color/palettes'; import { CustomColor } from '../../color/useForegroundColor'; import { createLineHeightFixNode } from '../../typography/createLineHeightFixNode'; @@ -34,6 +36,7 @@ export type TextProps = { ) & { style?: StyleProp; }; + export const Text = forwardRef, TextProps>(function Text( { align, @@ -53,7 +56,7 @@ export const Text = forwardRef, TextProps>(functio ref ) { useEffect(() => { - if (__DEV__) { + if (IS_DEV && !SILENCE_EMOJI_WARNINGS) { if (!containsEmojiProp && nodeHasEmoji(children)) { // eslint-disable-next-line no-console console.log( @@ -67,6 +70,7 @@ export const Text = forwardRef, TextProps>(functio ); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const textStyle = useTextStyle({ @@ -90,7 +94,7 @@ export const Text = forwardRef, TextProps>(functio testID={testID} onPress={onPress} > - {ios && containsEmojiProp && nodeIsString(children) ? renderStringWithEmoji(children) : children} + {IS_IOS && containsEmojiProp && nodeIsString(children) ? renderStringWithEmoji(children) : children} {lineHeightFixNode} ); diff --git a/src/design-system/components/TextIcon/TextIcon.tsx b/src/design-system/components/TextIcon/TextIcon.tsx index 381c7513b32..06d2a443753 100644 --- a/src/design-system/components/TextIcon/TextIcon.tsx +++ b/src/design-system/components/TextIcon/TextIcon.tsx @@ -8,7 +8,7 @@ import { Text, TextSize, TextWeight } from '../Text/Text'; export type TextIconProps = { align?: 'center' | 'left' | 'right'; - children: string | (string | null)[]; + children: string | (string | null)[] | React.ReactNode; color: TextColor | CustomColor; containerSize?: number; height?: number; diff --git a/src/design-system/components/TextShadow/TextShadow.tsx b/src/design-system/components/TextShadow/TextShadow.tsx new file mode 100644 index 00000000000..2b5a679d474 --- /dev/null +++ b/src/design-system/components/TextShadow/TextShadow.tsx @@ -0,0 +1,62 @@ +import React, { ReactElement, useMemo } from 'react'; +import { StyleProp, TextStyle, View, ViewStyle } from 'react-native'; +import { IS_ANDROID } from '@/env'; +import { opacity } from '@/__swaps__/utils/swaps'; +import { useColorMode } from '../../color/ColorMode'; +import { useForegroundColor } from '../../color/useForegroundColor'; +import { Text, TextProps } from '../Text/Text'; + +export interface TextShadowProps { + blur?: number; + children: ReactElement; + color?: string; + containerStyle?: StyleProp; + disabled?: boolean; + enableInLightMode?: boolean; + shadowOpacity?: number; + textStyle?: StyleProp; + x?: number; + y?: number; +} + +export const TextShadow = ({ + blur = 16, + children, + color, + containerStyle, + // ⚠️ TODO: Need to test on Android - defaulting to disabled on Android for now + disabled = IS_ANDROID, + enableInLightMode, + shadowOpacity = 0.6, + textStyle, + x = 0, + y = 0, +}: TextShadowProps) => { + const { isDarkMode } = useColorMode(); + + const inferredTextColor = useForegroundColor(children.props.color ?? 'label'); + const inferredTextSize = children.props.size || '17pt'; + + const [internalContainerStyle, internalTextStyle] = useMemo(() => { + const extraSpaceForShadow = blur + Math.max(Math.abs(x), Math.abs(y)); + return [ + { margin: -extraSpaceForShadow }, + { + textShadowColor: opacity(color || inferredTextColor, shadowOpacity), + textShadowOffset: { width: x, height: y }, + textShadowRadius: blur, + padding: extraSpaceForShadow, + }, + ]; + }, [blur, color, inferredTextColor, shadowOpacity, x, y]); + + return !disabled && (isDarkMode || enableInLightMode) ? ( + + + {children} + + + ) : ( + <>{children} + ); +}; diff --git a/src/design-system/index.ts b/src/design-system/index.ts index d7cfff76a8c..52d79657419 100644 --- a/src/design-system/index.ts +++ b/src/design-system/index.ts @@ -22,6 +22,7 @@ export { Stack } from './components/Stack/Stack'; export { selectTextSizes, Text } from './components/Text/Text'; export { TextIcon } from './components/TextIcon/TextIcon'; export { TextLink } from './components/TextLink/TextLink'; +export { TextShadow } from './components/TextShadow/TextShadow'; export { useColorMode } from './color/ColorMode'; export { useForegroundColor } from './color/useForegroundColor'; export { useHeadingStyle } from './components/Heading/useHeadingStyle'; @@ -47,3 +48,4 @@ export type { StackProps } from './components/Stack/Stack'; export type { TextLinkProps } from './components/TextLink/TextLink'; export type { TextProps } from './components/Text/Text'; export type { TextIconProps } from './components/TextIcon/TextIcon'; +export type { TextShadowProps } from './components/TextShadow/TextShadow'; From 47cbd3c9581d1af09e36128351fe7628add08b1d Mon Sep 17 00:00:00 2001 From: Jin Date: Fri, 14 Jun 2024 20:42:50 -0400 Subject: [PATCH 3/4] Revert "swap button states (#5785)" (#5852) 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; From 3de6fe716fb2efea3950400b39128124fdc1bac6 Mon Sep 17 00:00:00 2001 From: gregs Date: Sat, 15 Jun 2024 14:33:17 -0300 Subject: [PATCH 4/4] swap paste button in output field (#5804) * paste * remove console.log * better * colors * ok * review * comment * Revert "Lint on pre-commit (#5836)" This reverts commit d56ed46e7772cd51e54a0f8214947de01e48dd47. * fix dupes --------- Co-authored-by: Matthew Wall --- .../screens/Swap/components/SearchInput.tsx | 71 ++++++++++++------- .../Swap/hooks/useAnimatedSwapStyles.ts | 33 +++++---- src/__swaps__/utils/swaps.ts | 12 ++-- src/design-system/color/palettes.ts | 34 +++++---- 4 files changed, 90 insertions(+), 60 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index f4d9086bfb6..8fed7115252 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -1,3 +1,13 @@ +import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; +import { opacity } from '@/__swaps__/utils/swaps'; +import { Input } from '@/components/inputs'; +import { AnimatedText, Bleed, Box, Column, Columns, Text, useColorMode, useForegroundColor } from '@/design-system'; +import * as i18n from '@/languages'; +import { userAssetsStore } from '@/state/assets/userAssets'; +import { useSwapsStore } from '@/state/swaps/swapsStore'; +import Clipboard from '@react-native-clipboard/clipboard'; import React from 'react'; import Animated, { SharedValue, @@ -7,18 +17,10 @@ import Animated, { useAnimatedReaction, useAnimatedStyle, useDerivedValue, + useSharedValue, } from 'react-native-reanimated'; -import { Input } from '@/components/inputs'; -import { AnimatedText, Bleed, Box, Column, Columns, Text, useColorMode, useForegroundColor } from '@/design-system'; -import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; -import { opacity } from '@/__swaps__/utils/swaps'; -import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { userAssetsStore } from '@/state/assets/userAssets'; -import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { GestureHandlerV1Button } from './GestureHandlerV1Button'; import { useDebouncedCallback } from 'use-debounce'; -import { useSwapsStore } from '@/state/swaps/swapsStore'; -import * as i18n from '@/languages'; +import { GestureHandlerV1Button } from './GestureHandlerV1Button'; const AnimatedInput = Animated.createAnimatedComponent(Input); @@ -55,7 +57,10 @@ export const SearchInput = ({ const labelQuaternary = useForegroundColor('labelQuaternary'); const btnText = useDerivedValue(() => { - if ((inputProgress.value === 2 && !output) || (outputProgress.value === 2 && output)) { + if ( + (inputProgress.value === NavigationSteps.SEARCH_FOCUSED && !output) || + (outputProgress.value === NavigationSteps.SEARCH_FOCUSED && output) + ) { return CANCEL_LABEL; } @@ -63,17 +68,17 @@ export const SearchInput = ({ return CLOSE_LABEL; } - // ⚠️ TODO: Add paste functionality to the asset to buy list when no asset is selected - // return PASTE_LABEL; + return PASTE_LABEL; }); const buttonVisibilityStyle = useAnimatedStyle(() => { - const isSearchFocused = (output ? outputProgress : inputProgress).value === NavigationSteps.SEARCH_FOCUSED; - const isAssetSelected = output ? internalSelectedOutputAsset.value : internalSelectedInputAsset.value; + const isInputSearchFocused = inputProgress.value === NavigationSteps.SEARCH_FOCUSED; + const isInputAssetSelected = !!internalSelectedOutputAsset.value; + const isVisible = output || isInputSearchFocused || isInputAssetSelected; return { - opacity: isSearchFocused || isAssetSelected ? 1 : 0, - pointerEvents: isSearchFocused || isAssetSelected ? 'auto' : 'none', + opacity: isVisible ? 1 : 0, + pointerEvents: isVisible ? 'auto' : 'none', }; }); @@ -90,12 +95,12 @@ export const SearchInput = ({ (output && outputProgress.value === NavigationSteps.SEARCH_FOCUSED) ); + const pastedSearchInputValue = useSharedValue(''); const searchInputValue = useAnimatedProps(() => { // Removing the value when the input is focused allows the input to be reset to the correct value on blur const query = isSearchFocused.value ? undefined : ''; - return { - text: query, + text: pastedSearchInputValue.value || query, defaultValue: '', }; }); @@ -104,12 +109,24 @@ export const SearchInput = ({ () => isSearchFocused.value, (focused, prevFocused) => { if (focused === false && prevFocused === true) { + pastedSearchInputValue.value = ''; if (output) runOnJS(onOutputSearchQueryChange)(''); else runOnJS(onInputSearchQueryChange)(''); } } ); + const onPaste = () => { + Clipboard.getString().then(text => { + // to prevent users from mistakingly pasting long ass texts when copying the wrong thing + // we slice the string to 42 which is the size of a eth address, + // no token name query search should be that big anyway + const v = text.trim().slice(0, 42); + pastedSearchInputValue.value = v; + useSwapsStore.setState({ outputSearchQuery: v }); + }); + }; + return ( @@ -178,14 +195,20 @@ export const SearchInput = ({ (output ? outputSearchRef : inputSearchRef).current?.blur()} + onPressJS={() => { + (output ? outputSearchRef : inputSearchRef).current?.blur(); + }} onPressWorklet={() => { 'worklet'; - const isSearchFocused = - (output && outputProgress.value === NavigationSteps.SEARCH_FOCUSED) || - (!output && inputProgress.value === NavigationSteps.SEARCH_FOCUSED); + if (output && outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED && !internalSelectedOutputAsset.value) { + runOnJS(onPaste)(); + } - if (isSearchFocused || (output && internalSelectedOutputAsset.value) || (!output && internalSelectedInputAsset.value)) { + if ( + isSearchFocused.value || + (output && internalSelectedOutputAsset.value) || + (!output && internalSelectedInputAsset.value) + ) { handleExitSearchWorklet(); } }} diff --git a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts index 96a1361b4fa..01af65cc014 100644 --- a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts +++ b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts @@ -1,6 +1,4 @@ /* eslint-disable no-nested-ternary */ -import { SharedValue, interpolate, useAnimatedStyle, withSpring, withTiming } from 'react-native-reanimated'; -import { globalColors, useColorMode } from '@/design-system'; import { BASE_INPUT_HEIGHT, BOTTOM_ACTION_BAR_HEIGHT, @@ -14,14 +12,17 @@ import { fadeConfig, springConfig, } from '@/__swaps__/screens/Swap/constants'; -import { getColorValueForThemeWorklet, opacityWorklet } from '@/__swaps__/utils/swaps'; import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; +import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; +import { getColorValueForThemeWorklet, opacityWorklet } from '@/__swaps__/utils/swaps'; import { spinnerExitConfig } from '@/components/animations/AnimatedSpinner'; -import { NavigationSteps } from './useSwapNavigation'; +import { globalColors, useColorMode } from '@/design-system'; +import { foregroundColors } from '@/design-system/color/palettes'; import { IS_ANDROID } from '@/env'; import { safeAreaInsetValues } from '@/utils'; -import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; +import { SharedValue, interpolate, useAnimatedStyle, useDerivedValue, withSpring, withTiming } from 'react-native-reanimated'; +import { NavigationSteps } from './useSwapNavigation'; import { ChainId } from '@/__swaps__/types/chains'; const INSET_BOTTOM = IS_ANDROID ? getSoftMenuBarHeight() - 24 : safeAreaInsetValues.bottom + 16; @@ -213,9 +214,15 @@ export function useAnimatedSwapStyles({ }; }); + const isPasteMode = useDerivedValue( + () => outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED && !internalSelectedOutputAsset.value + ); + const searchOutputAssetButtonStyle = useAnimatedStyle(() => { + const color = isPasteMode.value ? foregroundColors.blue : internalSelectedOutputAsset.value?.highContrastColor; + return { - color: getColorValueForThemeWorklet(internalSelectedOutputAsset.value?.highContrastColor, isDarkMode, true), + color: getColorValueForThemeWorklet(color, isDarkMode, true), }; }); @@ -245,16 +252,12 @@ export function useAnimatedSwapStyles({ }); const searchOutputAssetButtonWrapperStyle = useAnimatedStyle(() => { + const color = isPasteMode.value ? foregroundColors.blue : internalSelectedOutputAsset.value?.highContrastColor; + return { - backgroundColor: opacityWorklet( - getColorValueForThemeWorklet(internalSelectedOutputAsset.value?.highContrastColor, isDarkMode, true), - isDarkMode ? 0.1 : 0.08 - ), - borderColor: opacityWorklet( - getColorValueForThemeWorklet(internalSelectedOutputAsset.value?.highContrastColor, isDarkMode, true), - isDarkMode ? 0.06 : 0.01 - ), - borderWidth: THICK_BORDER_WIDTH, + backgroundColor: opacityWorklet(getColorValueForThemeWorklet(color, isDarkMode, true), isDarkMode ? 0.1 : 0.08), + borderColor: opacityWorklet(getColorValueForThemeWorklet(color, isDarkMode, true), isDarkMode ? 0.06 : 0.01), + borderWidth: isPasteMode.value ? 0 : THICK_BORDER_WIDTH, }; }); diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 5a79cdba194..a5fe17f6d80 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -14,16 +14,16 @@ import { } from '@/__swaps__/screens/Swap/constants'; import { chainNameFromChainId, chainNameFromChainIdWorklet } from '@/__swaps__/utils/chains'; import { ChainId, ChainName } from '@/__swaps__/types/chains'; -import { RainbowConfig } from '@/model/remoteConfig'; -import { CrosschainQuote, ETH_ADDRESS, Quote, QuoteParams, SwapType, WRAPPED_ASSET } from '@rainbow-me/swaps'; import { isLowerCaseMatch } from '@/__swaps__/utils/strings'; -import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '../types/assets'; -import { inputKeys } from '../types/swap'; -import { swapsStore } from '../../state/swaps/swapsStore'; -import { BigNumberish } from '@ethersproject/bignumber'; import { TokenColors } from '@/graphql/__generated__/metadata'; +import { RainbowConfig } from '@/model/remoteConfig'; import { userAssetsStore } from '@/state/assets/userAssets'; import { colors } from '@/styles'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { CrosschainQuote, ETH_ADDRESS, Quote, QuoteParams, SwapType, WRAPPED_ASSET } from '@rainbow-me/swaps'; +import { swapsStore } from '../../state/swaps/swapsStore'; +import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '../types/assets'; +import { inputKeys } from '../types/swap'; import { convertAmountToRawAmount } from './numbers'; import { ceilWorklet, diff --git a/src/design-system/color/palettes.ts b/src/design-system/color/palettes.ts index a95f52491e9..ddfcf167913 100644 --- a/src/design-system/color/palettes.ts +++ b/src/design-system/color/palettes.ts @@ -200,7 +200,7 @@ export type BackgroundColorValue = { mode: ColorMode; }; -export const backgroundColors: Record> = { +export const backgroundColors: Record> = { 'surfacePrimary': { light: { color: globalColors.white100, @@ -400,8 +400,14 @@ export const backgroundColors: Record { +function selectBackgroundAsForeground(backgroundName: BackgroundColor): ContextualColorValue { const bg = backgroundColors[backgroundName]; - if ('color' in bg) { - return bg.color; - } - return { dark: bg.dark.color, light: bg.light.color, @@ -477,7 +479,7 @@ function selectBackgroundAsForeground(backgroundName: BackgroundColor): string | }; } -export const foregroundColors: Record> = { +export const foregroundColors: Record> = { 'label': { light: globalColors.grey100, dark: globalColors.white100, @@ -632,12 +634,18 @@ export const foregroundColors: Record { - if ('color' in value) { - return value; - } - if (colorMode === 'darkTinted') { return value.darkTinted ?? value.dark; }