Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swaps Rearchitecture #5705

Merged
merged 14 commits into from
May 8, 2024
1 change: 0 additions & 1 deletion src/__swaps__/screens/Swap/Swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export function SwapScreen() {
<ExchangeRateBubble />
<SwapWarning />
</Box>
<SwapAmountInputs />
</Box>
<SwapNavbar />
</Box>
Expand Down
54 changes: 36 additions & 18 deletions src/__swaps__/screens/Swap/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useMemo } from 'react';
import { TextInput } from 'react-native';
import Animated, { runOnUI, useAnimatedRef, useDerivedValue } from 'react-native-reanimated';
import React, { useCallback, useMemo } from 'react';
import { NativeSyntheticEvent, TextInputChangeEventData } from 'react-native';
import Animated, { useAnimatedProps, useDerivedValue } from 'react-native-reanimated';
import { ButtonPressAnimation } from '@/components/animations';
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 { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { userAssetsStore } from '@/state/assets/userAssets';

const AnimatedInput = Animated.createAnimatedComponent(Input);

Expand All @@ -21,11 +22,9 @@ export const SearchInput = ({
handleFocusSearch: () => void;
output?: boolean;
}) => {
const { inputProgress, outputProgress, SwapInputController, AnimatedSwapStyles } = useSwapContext();
const { searchInputRef, inputProgress, outputProgress, AnimatedSwapStyles } = useSwapContext();
const { isDarkMode } = useColorMode();

const inputRef = useAnimatedRef<TextInput>();

const fillTertiary = useForegroundColor('fillTertiary');
const label = useForegroundColor('label');
const labelQuaternary = useForegroundColor('labelQuaternary');
Expand All @@ -38,9 +37,22 @@ export const SearchInput = ({
return 'Close';
});

const initialValue = useMemo(() => {
return SwapInputController.searchQuery.value;
}, [SwapInputController.searchQuery.value]);
const defaultValue = useMemo(() => {
return userAssetsStore.getState().searchQuery;
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am most likely just misunderstanding but why do we need to set the default value to the initial searchQuery? Wouldn't the initial searchQuery just be ""?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that assumption is always true, good point.


const onSearchQueryChange = useCallback((event: NativeSyntheticEvent<TextInputChangeEventData>) => {
userAssetsStore.setState({ searchQuery: event.nativeEvent.text });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also unrelated to the main focus of this pr, but i feel like this would be cleaner if it just accepts a string. that way you wouldn't be creating artificial NativeSyntheticEvents below

}, []);

const searchInputValue = useAnimatedProps(() => {
const isFocused = inputProgress.value === 1 || outputProgress.value === 1;

// Removing the value when the input is focused allows the input to be reset to the correct value on blur
const query = isFocused ? undefined : defaultValue;

return { defaultValue, text: query };
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think default value needs to be an animated prop but we can change this later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but when I removed it typescript was yelling at me for whatever reason..


return (
<Box width="full">
Expand Down Expand Up @@ -68,18 +80,20 @@ export const SearchInput = ({
</Box>
</Column>
<AnimatedInput
onChange={e => {
runOnUI(SwapInputController.onChangeSearchQuery)(e.nativeEvent.text);
}}
animatedProps={searchInputValue}
onChange={onSearchQueryChange}
onBlur={() => {
onSearchQueryChange({
nativeEvent: {
text: '',
},
} as NativeSyntheticEvent<TextInputChangeEventData>);
handleExitSearch();
}}
onFocus={() => {
handleFocusSearch();
}}
onFocus={handleFocusSearch}
placeholder={output ? 'Find a token to buy' : 'Search your tokens'}
placeholderTextColor={isDarkMode ? opacity(labelQuaternary, 0.3) : labelQuaternary}
ref={inputRef}
ref={searchInputRef}
selectionColor={color}
spellCheck={false}
style={{
Expand All @@ -89,7 +103,6 @@ export const SearchInput = ({
height: 44,
zIndex: 10,
}}
defaultValue={initialValue}
/>
</Columns>
</Box>
Expand All @@ -98,8 +111,13 @@ export const SearchInput = ({
<Column width="content">
<ButtonPressAnimation
onPress={() => {
onSearchQueryChange({
nativeEvent: {
text: '',
},
} as NativeSyntheticEvent<TextInputChangeEventData>);
handleExitSearch();
inputRef.current?.blur();
searchInputRef.current?.blur();
}}
scaleTo={0.8}
>
Expand Down
8 changes: 4 additions & 4 deletions src/__swaps__/screens/Swap/components/SwapBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ export const SwapBackground = () => {

const fallbackColor = isDarkMode ? ETH_COLOR_DARK : ETH_COLOR;

const bottomColorDarkened = useSharedValue(getTintedBackgroundColor(fallbackColor, isDarkMode));
const topColorDarkened = useSharedValue(getTintedBackgroundColor(fallbackColor, isDarkMode));
const bottomColorDarkened = useSharedValue(getTintedBackgroundColor(fallbackColor)[isDarkMode ? 'dark' : 'light']);
const topColorDarkened = useSharedValue(getTintedBackgroundColor(fallbackColor)[isDarkMode ? 'dark' : 'light']);

const getDarkenedColors = ({ topColor, bottomColor }: { topColor: string; bottomColor: string }) => {
bottomColorDarkened.value = getTintedBackgroundColor(bottomColor, isDarkMode);
topColorDarkened.value = getTintedBackgroundColor(topColor, isDarkMode);
bottomColorDarkened.value = getTintedBackgroundColor(bottomColor)[isDarkMode ? 'dark' : 'light'];
topColorDarkened.value = getTintedBackgroundColor(topColor)[isDarkMode ? 'dark' : 'light'];
};

useAnimatedReaction(
Expand Down
14 changes: 6 additions & 8 deletions src/__swaps__/screens/Swap/components/SwapSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { clamp, opacity, opacityWorklet } from '@/__swaps__/utils/swaps';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon';
import { useTheme } from '@/theme';
import { useSwapAssetStore } from '@/__swaps__/screens/Swap/state/assets';
import { ethereumUtils } from '@/utils';
import { ChainId } from '@/__swaps__/types/chains';

Expand All @@ -61,8 +60,6 @@ export const SwapSlider = ({
const { isDarkMode } = useColorMode();
const { SwapInputController, sliderXPosition, sliderPressProgress } = useSwapContext();

const { assetToSell } = useSwapAssetStore();

const panRef = useRef();
const tapRef = useRef();

Expand Down Expand Up @@ -377,14 +374,15 @@ export const SwapSlider = ({
<Columns alignHorizontal="justify" alignVertical="center">
<Inline alignVertical="center" space="6px" wrap={false}>
<Bleed vertical="4px">
{/* TODO: Migrate this to fast icon image with shared value once we have that */}
<SwapCoinIcon
color={SwapInputController.topColorShadow.value}
iconUrl={assetToSell?.icon_url}
address={assetToSell?.address ?? ''}
mainnetAddress={assetToSell?.mainnetAddress ?? ''}
network={ethereumUtils.getNetworkFromChainId(Number(assetToSell?.chainId ?? ChainId.mainnet))}
iconUrl={''}
address={''}
mainnetAddress={''}
network={ethereumUtils.getNetworkFromChainId(ChainId.mainnet)}
small
symbol={assetToSell?.symbol ?? ''}
symbol={''}
theme={theme}
/>
</Bleed>
Expand Down
54 changes: 29 additions & 25 deletions src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import c from 'chroma-js';
import * as i18n from '@/languages';
import { Text as RNText, StyleSheet } from 'react-native';
import Animated, { SharedValue, runOnUI, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
import Animated, { runOnUI, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
import React, { useCallback, useMemo } from 'react';

import { SUPPORTED_CHAINS } from '@/references';
Expand All @@ -20,8 +20,8 @@ import { ChainId, ChainName } from '@/__swaps__/types/chains';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { ContextMenuButton } from '@/components/context-menu';
import { useAccountAccentColor } from '@/hooks';
import { UserAssetFilter } from '@/__swaps__/types/assets';
import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu';
import { userAssetsStore } from '@/state/assets/userAssets';

type ChainSelectionProps = {
allText?: string;
Expand All @@ -31,9 +31,13 @@ type ChainSelectionProps = {
export const ChainSelection = ({ allText, output }: ChainSelectionProps) => {
const { isDarkMode } = useColorMode();
const { accentColor: accountColor } = useAccountAccentColor();
const { SwapInputController, userAssetFilter } = useSwapContext();
const { outputChainId } = useSwapContext();
const red = useForegroundColor('red');

const initialFilter = useMemo(() => {
return userAssetsStore.getState().filter;
}, []);

const accentColor = useMemo(() => {
if (c.contrast(accountColor, isDarkMode ? '#191A1C' : globalColors.white100) < (isDarkMode ? 2.125 : 1.5)) {
const shiftedColor = isDarkMode ? c(accountColor).brighten(1).saturate(0.5).css() : c(accountColor).darken(0.5).saturate(0.5).css();
Expand All @@ -43,38 +47,42 @@ export const ChainSelection = ({ allText, output }: ChainSelectionProps) => {
}
}, [accountColor, isDarkMode]);

const propToSet = output ? SwapInputController.outputChainId : userAssetFilter;

const chainName = useSharedValue(
propToSet.value === 'all' ? allText : propToSet.value === ChainId.mainnet ? 'ethereum' : chainNameFromChainIdWorklet(propToSet.value)
output
? chainNameFromChainIdWorklet(outputChainId.value)
: initialFilter === 'all'
? allText
: chainNameFromChainIdWorklet(initialFilter as ChainId)
);

useAnimatedReaction(
() => ({
outputChainId: SwapInputController.outputChainId.value,
userAssetFilter: userAssetFilter.value,
outputChainId: outputChainId.value,
}),
current => {
if (output) {
chainName.value = chainNameForChainIdWithMainnetSubstitutionWorklet(current.outputChainId);
} else {
chainName.value =
current.userAssetFilter === 'all' ? allText : chainNameForChainIdWithMainnetSubstitutionWorklet(current.userAssetFilter);
}
}
);

const handleSelectChain = useCallback(
({ nativeEvent: { actionKey } }: Omit<OnPressMenuItemEventObject, 'isUsingActionSheetFallback'>) => {
runOnUI((set: SharedValue<ChainId | UserAssetFilter>) => {
if (actionKey === 'all') {
set.value = 'all';
} else {
set.value = Number(actionKey) as ChainId;
}
})(propToSet);
if (output) {
runOnUI(() => {
outputChainId.value = Number(actionKey) as ChainId;
chainName.value = chainNameForChainIdWithMainnetSubstitutionWorklet(Number(actionKey) as ChainId);
});
} else {
userAssetsStore.setState({
filter: actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId),
});
runOnUI(() => {
chainName.value = actionKey === 'all' ? allText : chainNameForChainIdWithMainnetSubstitutionWorklet(Number(actionKey) as ChainId);
});
}
},
[propToSet]
[allText, chainName, output, outputChainId]
);

const menuConfig = useMemo(() => {
Expand Down Expand Up @@ -197,12 +205,8 @@ export const ChainSelection = ({ allText, output }: ChainSelectionProps) => {
>
<HitSlop space="10px">
<Inline alignVertical="center" space="6px" wrap={false}>
{output && (
<ChainImage
chain={ethereumUtils.getNetworkFromChainId(SwapInputController.outputChainId.value ?? ChainId.mainnet)}
size={16}
/>
)}
{/* TODO: We need to add some ethereum utils to handle worklet functions */}
{output && <ChainImage chain={ethereumUtils.getNetworkFromChainId(outputChainId.value ?? ChainId.mainnet)} size={16} />}
<AnimatedText
align="right"
color={isDarkMode ? 'labelSecondary' : 'label'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { AssetToBuySection, AssetToBuySectionId } from '@/__swaps__/screens/Swap
import { ChainId } from '@/__swaps__/types/chains';
import { TextColor } from '@/design-system/color/palettes';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { parseSearchAsset, isSameAsset } from '@/__swaps__/utils/assets';
import { parseSearchAsset } from '@/__swaps__/utils/assets';

import { useAssetsToSell } from '@/__swaps__/screens/Swap/hooks/useAssetsToSell';
import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty';
import { SwapAssetType } from '@/__swaps__/types/swap';
import { userAssetsStore } from '@/state/assets/userAssets';

interface SectionProp {
color: TextStyle['color'];
Expand Down Expand Up @@ -65,21 +66,23 @@ const bridgeSectionsColorsByChain = {
const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList<SearchAsset>);

export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) => {
const { SwapInputController } = useSwapContext();
const userAssets = useAssetsToSell();
const { setAsset, outputChainId } = useSwapContext();

const handleSelectToken = useCallback(
(token: SearchAsset) => {
const userAsset = userAssets.find(asset => isSameAsset(asset, token));
const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId);
const parsedAsset = parseSearchAsset({
assetWithPrice: undefined,
searchAsset: token,
userAsset,
});

SwapInputController.onSetAssetToBuy(parsedAsset);
setAsset({
type: SwapAssetType.outputAsset,
asset: parsedAsset,
});
},
[SwapInputController, userAssets]
[setAsset]
);

const { symbol, title } = sectionProps[section.id];
Expand All @@ -91,7 +94,7 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) =
return sectionProps[section.id].color as TextColor;
}

return bridgeSectionsColorsByChain[SwapInputController.outputChainId.value || ChainId.mainnet] as TextColor;
return bridgeSectionsColorsByChain[outputChainId.value || ChainId.mainnet] as TextColor;
});

if (!section.data.length) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,36 @@ import { CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow';
import { useAssetsToSell } from '@/__swaps__/screens/Swap/hooks/useAssetsToSell';
import { ParsedSearchAsset } from '@/__swaps__/types/assets';
import { Stack } from '@/design-system';
import Animated, { runOnUI } from 'react-native-reanimated';
import Animated from 'react-native-reanimated';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { parseSearchAsset, isSameAsset } from '@/__swaps__/utils/assets';
import { parseSearchAsset } from '@/__swaps__/utils/assets';
import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty';
import { FlashList } from '@shopify/flash-list';
import { ChainSelection } from './ChainSelection';
import { SwapAssetType } from '@/__swaps__/types/swap';
import { userAssetsStore } from '@/state/assets/userAssets';

const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList<ParsedSearchAsset>);

export const TokenToSellList = () => {
const { SwapInputController } = useSwapContext();
const { setAsset } = useSwapContext();
const userAssets = useAssetsToSell();

const handleSelectToken = useCallback(
(token: ParsedSearchAsset) => {
const userAsset = userAssets.find(asset => isSameAsset(asset, token));
const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId);
const parsedAsset = parseSearchAsset({
assetWithPrice: undefined,
searchAsset: token,
userAsset,
});

runOnUI(SwapInputController.onSetAssetToSell)(parsedAsset);
setAsset({
type: SwapAssetType.inputAsset,
asset: parsedAsset,
});
},
[SwapInputController.onSetAssetToSell, userAssets]
[setAsset]
);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import Animated, {
runOnJS,
runOnUI,
useAnimatedGestureHandler,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
import { useAnimatedGestureHandler, useSharedValue } from 'react-native-reanimated';
import { useSwapContext } from '../../providers/swap-provider';

export const useSwapActionsGestureHandler = () => {
Expand Down
1 change: 0 additions & 1 deletion src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { spinnerExitConfig } from '@/__swaps__/components/animations/AnimatedSpi
import { NavigationSteps } from './useSwapNavigation';
import { IS_ANDROID } from '@/env';
import { safeAreaInsetValues } from '@/utils';
import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android';

export function useAnimatedSwapStyles({
SwapInputController,
Expand Down
Loading
Loading