Skip to content

Commit afad4dd

Browse files
christianbaroniderHowie
authored andcommitted
Swaps fixes (#6573)
* Rework `navigateToSwaps` * Swaps bug fixes, performance improvements * Clean up remote config, fix slippage types * Clean up `AnimatedSpinner` image on unmount * Push missing files, lint fixes * Lint * Separate chain badge improvements * Separate user assets store type fixes, clean up error supression * Ensure `getSwapsNavigationParams` always returns valid params Haven't ever seen this happen, just in case due to the current lack of proper navigation typing * Fix swaps network selection input handling * Prevent swaps inputs from clearing unless `Cancel` is pressed, rework network selector handling * Prevent swap input re-render * Limit erratic gas state updates, propagate balance updates in swaps after account switches * Allow user assets to refresh while swaps is open * Remove unnecessary `useCleanupOnUnmount` component wrapper * Fix remaining `hasEnoughFundsForGas` / `maxSwappableAmount` edge cases * Reduce size of `FlipButton` hit box * Handle negative `decimalPlaces` in `scaleUpWorklet`
1 parent 21dd350 commit afad4dd

File tree

63 files changed

+2303
-1875
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2303
-1875
lines changed

src/__swaps__/screens/Swap/Swap.tsx

Lines changed: 44 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import React, { useCallback, useEffect, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
22
import { StatusBar, StyleSheet } from 'react-native';
3-
import Animated, { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
3+
import Animated from 'react-native-reanimated';
44
import { ScreenCornerRadius } from 'react-native-screen-corner-radius';
5-
5+
import { AbsolutePortalRoot } from '@/components/AbsolutePortal';
66
import { Page } from '@/components/layout';
77
import { navbarHeight } from '@/components/navbar/Navbar';
88
import { DecoyScrollView } from '@/components/sheet/DecoyScrollView';
99
import { Box } from '@/design-system';
1010
import { IS_ANDROID } from '@/env';
11-
import { safeAreaInsetValues } from '@/utils';
12-
11+
import { useDelayedMount } from '@/hooks/useDelayedMount';
12+
import { userAssetsStore } from '@/state/assets/userAssets';
13+
import { userAssetsStoreManager } from '@/state/assets/userAssetsStoreManager';
14+
import { ChainId } from '@/state/backendNetworks/types';
15+
import { useSwapsStore } from '@/state/swaps/swapsStore';
1316
import { ExchangeRateBubble } from '@/__swaps__/screens/Swap/components/ExchangeRateBubble';
1417
import { FlipButton } from '@/__swaps__/screens/Swap/components/FlipButton';
1518
import { SliderAndKeyboard } from '@/__swaps__/screens/Swap/components/SliderAndKeyboard';
@@ -18,18 +21,13 @@ import { SwapBottomPanel } from '@/__swaps__/screens/Swap/components/SwapBottomP
1821
import { SwapInputAsset } from '@/__swaps__/screens/Swap/components/SwapInputAsset';
1922
import { SwapNavbar } from '@/__swaps__/screens/Swap/components/SwapNavbar';
2023
import { SwapOutputAsset } from '@/__swaps__/screens/Swap/components/SwapOutputAsset';
21-
import { ChainId } from '@/state/backendNetworks/types';
2224
import { SwapAssetType } from '@/__swaps__/types/swap';
23-
import { parseSearchAsset } from '@/__swaps__/utils/assets';
24-
import { AbsolutePortalRoot } from '@/components/AbsolutePortal';
25-
import { useAccountSettings } from '@/hooks';
26-
import { useDelayedMount } from '@/hooks/useDelayedMount';
27-
import { userAssetsStore } from '@/state/assets/userAssets';
28-
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
25+
import { parseAssetAndExtend } from '@/__swaps__/utils/swaps';
26+
import { safeAreaInsetValues } from '@/utils';
27+
import { NavigateToSwapSettingsTrigger } from './components/NavigateToSwapSettingsTrigger';
2928
import { SwapWarning } from './components/SwapWarning';
3029
import { clearCustomGasSettings } from './hooks/useCustomGas';
3130
import { SwapProvider, useSwapContext } from './providers/swap-provider';
32-
import { NavigateToSwapSettingsTrigger } from './components/NavigateToSwapSettingsTrigger';
3331
import { useSwapsSearchStore } from './resources/search/searchV2';
3432
import { ReviewButton } from './components/ReviewButton';
3533

@@ -71,9 +69,9 @@ import { ReviewButton } from './components/ReviewButton';
7169
*/
7270

7371
export function SwapScreen() {
72+
useCleanupOnUnmount();
7473
return (
7574
<SwapProvider>
76-
<MountAndUnmountHandlers />
7775
<Box as={Page} style={styles.rootViewBackground} testID="swap-screen" width="full">
7876
<SwapBackground />
7977
<Box alignItems="center" height="full" paddingTop={{ custom: safeAreaInsetValues.top + (navbarHeight - 12) + 29 }} width="full">
@@ -93,87 +91,59 @@ export function SwapScreen() {
9391
);
9492
}
9593

96-
const MountAndUnmountHandlers = () => {
97-
useMountSignal();
98-
useCleanupOnUnmount();
99-
100-
return null;
101-
};
102-
103-
const useMountSignal = () => {
104-
useEffect(() => {
105-
useSwapsStore.setState(state => ({
106-
...state,
107-
isSwapsOpen: true,
108-
selectedOutputChainId: state?.inputAsset?.chainId ?? state?.preferredNetwork ?? state?.selectedOutputChainId ?? ChainId.mainnet,
109-
}));
110-
}, []);
111-
};
112-
11394
const useCleanupOnUnmount = () => {
11495
useEffect(() => {
11596
return () => {
11697
const highestValueEth = userAssetsStore.getState().getHighestValueNativeAsset();
117-
const preferredNetwork = swapsStore.getState().preferredNetwork;
118-
const parsedAsset = highestValueEth
119-
? parseSearchAsset({
120-
assetWithPrice: undefined,
121-
searchAsset: highestValueEth,
122-
userAsset: highestValueEth,
123-
})
124-
: null;
125-
126-
useSwapsStore.setState({
127-
inputAsset: parsedAsset,
128-
isSwapsOpen: false,
129-
outputAsset: null,
130-
quote: null,
131-
selectedOutputChainId: parsedAsset?.chainId ?? preferredNetwork ?? ChainId.mainnet,
132-
quickBuyAnalyticalData: undefined,
133-
lastNavigatedTrendingToken: undefined,
98+
99+
useSwapsStore.setState(state => {
100+
const didInputAssetChange = state.inputAsset?.uniqueId !== highestValueEth?.uniqueId;
101+
const inputAsset = didInputAssetChange ? parseAssetAndExtend({ asset: highestValueEth }) : state.inputAsset;
102+
return {
103+
inputAsset,
104+
isSwapsOpen: false,
105+
lastNavigatedTrendingToken: undefined,
106+
outputAsset: null,
107+
quote: null,
108+
selectedOutputChainId: inputAsset?.chainId ?? state.preferredNetwork ?? ChainId.mainnet,
109+
};
134110
});
135111

136-
useSwapsSearchStore.setState({ searchQuery: '' });
137-
userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' });
112+
useSwapsSearchStore.setState(state => (state.searchQuery.length ? { searchQuery: '' } : state));
113+
userAssetsStore.setState(state =>
114+
state.filter === 'all' && !state.inputSearchQuery.length ? state : { filter: 'all', inputSearchQuery: '' }
115+
);
138116

139117
clearCustomGasSettings();
140118
};
141119
}, []);
142120
};
143121

144122
const WalletAddressObserver = () => {
145-
const { accountAddress } = useAccountSettings();
146-
const { setAsset } = useSwapContext();
123+
const { hasEnoughFundsForGas, setAsset } = useSwapContext();
124+
const accountAddress = userAssetsStoreManager(state => state.address);
125+
const lastAccountAddress = useRef(accountAddress);
147126

148127
const setNewInputAsset = useCallback(() => {
149-
const newHighestValueEth = userAssetsStore.getState().getHighestValueNativeAsset();
128+
const { filter, getHighestValueNativeAsset, userAssets } = userAssetsStore.getState();
150129

151-
if (userAssetsStore.getState().filter !== 'all') {
152-
userAssetsStore.setState({ filter: 'all' });
153-
}
130+
if (filter !== 'all') userAssetsStore.setState({ filter: 'all' });
131+
const hasAssets = userAssets.size > 0;
154132

155133
setAsset({
134+
asset: hasAssets ? getHighestValueNativeAsset() : null,
135+
didWalletChange: true,
156136
type: SwapAssetType.inputAsset,
157-
asset: newHighestValueEth,
158137
});
159-
160-
if (userAssetsStore.getState().userAssets.size === 0) {
161-
setAsset({
162-
type: SwapAssetType.outputAsset,
163-
asset: null,
164-
});
165-
}
166138
}, [setAsset]);
167139

168-
useAnimatedReaction(
169-
() => accountAddress,
170-
(current, previous) => {
171-
const didWalletAddressChange = previous && current !== previous;
172-
173-
if (didWalletAddressChange) runOnJS(setNewInputAsset)();
174-
},
175-
[]
176-
);
140+
useEffect(() => {
141+
if (accountAddress !== lastAccountAddress.current) {
142+
hasEnoughFundsForGas.value = undefined;
143+
lastAccountAddress.current = accountAddress;
144+
setNewInputAsset();
145+
}
146+
}, [accountAddress, hasEnoughFundsForGas, setNewInputAsset]);
177147

178148
return null;
179149
};
@@ -184,7 +154,7 @@ const areBothAssetsPrefilled = () => {
184154
};
185155

186156
const SliderAndKeyboardAndBottomControls = () => {
187-
const skipDelayedMount = useMemo(() => areBothAssetsPrefilled(), []);
157+
const [skipDelayedMount] = useState(() => areBothAssetsPrefilled());
188158
const shouldMount = useDelayedMount({ skipDelayedMount });
189159

190160
const { AnimatedSwapStyles } = useSwapContext();

src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-var-requires */
21
import React, { useMemo } from 'react';
32
import { Image, View } from 'react-native';
43
import { getChainBadgeStyles } from '@/components/coin-icon/ChainImage';

src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-nested-ternary */
21
import React, { memo } from 'react';
32
import { StyleSheet, View, ViewStyle } from 'react-native';
43
import { borders } from '@/styles';
@@ -9,10 +8,11 @@ import { AnimatedFasterImage } from '@/components/AnimatedComponents/AnimatedFas
98
import { AnimatedChainImage } from './AnimatedChainImage';
109
import { fadeConfig } from '../constants';
1110
import { SwapCoinIconTextFallback } from './SwapCoinIconTextFallback';
12-
import { Box } from '@/design-system';
11+
import { Box, globalColors, useColorMode } from '@/design-system';
1312
import { IS_ANDROID, IS_IOS } from '@/env';
1413
import { PIXEL_RATIO } from '@/utils/deviceUtils';
1514
import { useSwapContext } from '../providers/swap-provider';
15+
import { getColorValueForThemeWorklet } from '@/__swaps__/utils/swaps';
1616

1717
export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({
1818
assetType,
@@ -25,7 +25,8 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({
2525
chainSize?: number;
2626
showBadge?: boolean;
2727
}) {
28-
const { isDarkMode, colors } = useTheme();
28+
const { isDarkMode } = useColorMode();
29+
const { colors } = useTheme();
2930
const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext();
3031

3132
const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset;
@@ -53,9 +54,20 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({
5354
return { showCoinIcon, showEmptyState, showFallback };
5455
});
5556

56-
const animatedCoinIconWrapperStyles = useAnimatedStyle(() => ({
57-
shadowColor: visibility.value.showCoinIcon ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent',
58-
}));
57+
const animatedCoinIconWrapperStyles = useAnimatedStyle(() => {
58+
const assetBackgroundColor = (assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset).value
59+
?.tintedBackgroundColor;
60+
const backgroundColor = assetBackgroundColor
61+
? isDarkMode
62+
? getColorValueForThemeWorklet(assetBackgroundColor, isDarkMode)
63+
: globalColors.white100
64+
: 'transparent';
65+
66+
return {
67+
backgroundColor,
68+
shadowColor: visibility.value.showCoinIcon ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent',
69+
};
70+
});
5971

6072
const animatedCoinIconStyles = useAnimatedStyle(() => ({
6173
display: visibility.value.showCoinIcon ? 'flex' : 'none',

src/__swaps__/screens/Swap/components/BalanceBadge.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-nested-ternary */
21
import React from 'react';
32
import { DerivedValue, useAnimatedStyle } from 'react-native-reanimated';
43
import { AnimatedText, Bleed, Box, useColorMode } from '@/design-system';

src/__swaps__/screens/Swap/components/CoinRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ const InfoButton = ({
254254
]
255255
: []),
256256
],
257-
menuTitle: '',
257+
menuTitle: `${isVerified ? i18n.t(i18n.l.token_search.section_header.verified) : i18n.t(i18n.l.token_search.section_header.unverified)} ${i18n.t(i18n.l.exchange.coin_row.token)}`,
258258
};
259259

260260
return { options, menuConfig };

src/__swaps__/screens/Swap/components/CoinRowButton.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-nested-ternary */
21
import React from 'react';
32
import { ButtonPressAnimation } from '@/components/animations';
43
import { Box, TextIcon, useColorMode, useForegroundColor } from '@/design-system';

src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ import { ETH_ADDRESS } from '@/references';
1111
import { GestureHandlerButton } from './GestureHandlerButton';
1212
import { convertAmountToNativeDisplayWorklet } from '@/helpers/utilities';
1313
import { useAccountSettings } from '@/hooks';
14+
import { StyleSheet } from 'react-native';
15+
import { DEVICE_WIDTH } from '@/utils/deviceUtils';
1416

1517
export const ExchangeRateBubble = () => {
1618
const { isDarkMode } = useColorMode();
17-
const { AnimatedSwapStyles, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching } = useSwapContext();
19+
const {
20+
AnimatedSwapStyles,
21+
SwapInputController: { inputNativePrice, outputNativePrice },
22+
internalSelectedInputAsset,
23+
internalSelectedOutputAsset,
24+
isFetching,
25+
} = useSwapContext();
1826
const { nativeCurrency: currentCurrency } = useAccountSettings();
1927

2028
const rotatingIndex = useSharedValue(0);
@@ -52,11 +60,14 @@ export const ExchangeRateBubble = () => {
5260
rotatingIndex: rotatingIndex.value,
5361
}),
5462
(current, previous) => {
63+
const inputAssetPrice = inputNativePrice.value;
64+
const outputAssetPrice = outputNativePrice.value;
65+
5566
if (
5667
!internalSelectedInputAsset.value ||
5768
!internalSelectedOutputAsset.value ||
58-
!internalSelectedInputAsset.value.nativePrice ||
59-
!internalSelectedOutputAsset.value.nativePrice ||
69+
!inputAssetPrice ||
70+
!outputAssetPrice ||
6071
current.inputAssetUniqueId !== previous?.inputAssetUniqueId ||
6172
current.outputAssetUniqueId !== previous?.outputAssetUniqueId
6273
) {
@@ -68,8 +79,8 @@ export const ExchangeRateBubble = () => {
6879
return;
6980
}
7081

71-
const { symbol: inputAssetSymbol, nativePrice: inputAssetPrice, type: inputAssetType } = internalSelectedInputAsset.value;
72-
const { symbol: outputAssetSymbol, nativePrice: outputAssetPrice, type: outputAssetType } = internalSelectedOutputAsset.value;
82+
const { symbol: inputAssetSymbol, type: inputAssetType } = internalSelectedInputAsset.value;
83+
const { symbol: outputAssetSymbol, type: outputAssetType } = internalSelectedOutputAsset.value;
7384

7485
const isInputAssetStablecoin = inputAssetType === 'stablecoin';
7586
const isOutputAssetStablecoin = outputAssetType === 'stablecoin';
@@ -105,7 +116,7 @@ export const ExchangeRateBubble = () => {
105116
}
106117
case 1: {
107118
const formattedRate = valueBasedDecimalFormatter({
108-
amount: outputAssetPrice / inputAssetPrice,
119+
amount: inputAssetPrice / outputAssetPrice,
109120
nativePrice: inputAssetPrice,
110121
roundingMode: 'up',
111122
precisionAdjustment: -1,
@@ -147,9 +158,10 @@ export const ExchangeRateBubble = () => {
147158

148159
return (
149160
<GestureHandlerButton
161+
hapticTrigger="tap-end"
162+
hitSlop={{ left: 24, right: 24, top: 12, bottom: 12 }}
150163
onPressWorklet={onChangeIndex}
151164
scaleTo={0.9}
152-
hitSlop={{ left: 24, right: 24, top: 12, bottom: 12 }}
153165
style={pointerEventsStyle}
154166
>
155167
<Box as={Animated.View} alignItems="center" justifyContent="center" style={AnimatedSwapStyles.hideWhenInputsExpandedOrPriceImpact}>
@@ -189,3 +201,16 @@ export const ExchangeRateBubble = () => {
189201
</GestureHandlerButton>
190202
);
191203
};
204+
205+
const styles = StyleSheet.create({
206+
buttonPadding: {
207+
paddingHorizontal: 24,
208+
paddingVertical: 12,
209+
},
210+
buttonPosition: {
211+
alignSelf: 'center',
212+
minWidth: DEVICE_WIDTH * 0.6,
213+
position: 'absolute',
214+
top: 4,
215+
},
216+
});

0 commit comments

Comments
 (0)