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; }