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: logic, data flow, and performance fixes #5787

Merged
merged 39 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9031a8e
Add pause functionality to useAnimatedTime
christianbaroni May 29, 2024
1cf77b9
useSyncSharedValue: allow pauseSync to be a boolean
christianbaroni May 29, 2024
d568c25
Create useDelayedMount hook
christianbaroni May 29, 2024
283dba4
Fix swap status bar handling
christianbaroni May 29, 2024
936a3e4
Refactor FastFallbackCoinIconImage to use faster-image
christianbaroni May 29, 2024
d80d2a7
Fix animated coin icon jank
christianbaroni May 30, 2024
1f45265
Lots of logic, performance, UI fixes
christianbaroni May 30, 2024
cbe04fd
Remove commented code
christianbaroni May 30, 2024
f400dd7
Merge branch 'develop' into @christian/swaps-fixes
christianbaroni May 30, 2024
b5d51c1
Fix shared value dependency
christianbaroni May 30, 2024
df75557
Fix initialInputNativeValue formatting
christianbaroni May 30, 2024
5d6fbad
lint: uncomment store favorites
christianbaroni May 30, 2024
6c15099
Update default swaps chain order
christianbaroni May 30, 2024
4a66612
Prevent NaN input values
christianbaroni May 30, 2024
4983197
Memoize UserAssetsSync
christianbaroni May 30, 2024
833fe3b
cleanup routes
walmat May 30, 2024
354d59a
fix double all networks on android
walmat May 30, 2024
dd90c84
Merge branch 'develop' into @christian/swaps-fixes
walmat May 30, 2024
bf8abe0
fix APP-1535
walmat May 30, 2024
6ba95bb
Merge branch '@christian/swaps-fixes' of https://github.com/rainbow-m…
walmat May 30, 2024
4624fd9
"Fetching Prices" → "Fetching"
christianbaroni May 31, 2024
e70dc6b
Delete userAssetsStore favorites migration
christianbaroni May 31, 2024
9f11757
Fix remaining logic flaws, userAssetsStore improvements, misc. fixes
christianbaroni May 31, 2024
51cdcdc
Consolidate useSwapEstimatedGasFee state updates
christianbaroni May 31, 2024
78882b1
Clean up EstimatedSwapGasFee styles
christianbaroni May 31, 2024
6577e90
Remove redundant memoization
christianbaroni May 31, 2024
a9ea6bf
Fix output asset balance and badge
christianbaroni May 31, 2024
484b638
Clear output asset when wallet address changes if no user assets
christianbaroni May 31, 2024
39d82d2
Use userAssets store for tokenToSellList data; fast search; fix searc…
christianbaroni May 31, 2024
27363e5
Add animated coin icon border radius back into animatedIconSource on …
christianbaroni May 31, 2024
5140f5d
Remove log
christianbaroni May 31, 2024
7eb8126
Fix coin row token balance
christianbaroni May 31, 2024
b57ff0b
Fix lint error — add toScaledIntegerWorklet
christianbaroni May 31, 2024
b270fbd
Merge branch 'develop' into @christian/swaps-fixes
christianbaroni May 31, 2024
e9b372a
Merge branch 'develop' into @christian/swaps-fixes
christianbaroni May 31, 2024
fd16c4b
Use token decimals for balance comparison
christianbaroni May 31, 2024
0b0491f
Fix toScaledIntegerWorklet scaling, add tests
christianbaroni May 31, 2024
cc7a642
Persist associatedWalletAddress, chainBalances, bump store version
christianbaroni May 31, 2024
07b47b7
Fix userAssetsQuery errors
christianbaroni May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 123 additions & 30 deletions src/__swaps__/screens/Swap/Swap.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import React, { useCallback, useEffect } from 'react';
import { StyleSheet, StatusBar } from 'react-native';
import Animated from 'react-native-reanimated';
import Animated, { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { ScreenCornerRadius } from 'react-native-screen-corner-radius';

import { IS_ANDROID } from '@/env';
Expand All @@ -19,8 +19,13 @@ import { SwapNavbar } from '@/__swaps__/screens/Swap/components/SwapNavbar';
import { SliderAndKeyboard } from '@/__swaps__/screens/Swap/components/SliderAndKeyboard';
import { SwapBottomPanel } from '@/__swaps__/screens/Swap/components/SwapBottomPanel';
import { SwapWarning } from './components/SwapWarning';
import { useSwapContext } from './providers/swap-provider';
import { UserAssetsSync } from './components/UserAssetsSync';
import { SwapProvider, useSwapContext } from './providers/swap-provider';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { userAssetsStore } from '@/state/assets/userAssets';
import { parseSearchAsset } from '@/__swaps__/utils/assets';
import { SwapAssetType } from '@/__swaps__/types/swap';
import { ChainId } from '@/__swaps__/types/chains';
import { useDelayedMount } from '@/hooks/useDelayedMount';

/** README
* This prototype is largely driven by Reanimated and Gesture Handler, which
Expand Down Expand Up @@ -60,38 +65,126 @@ import { UserAssetsSync } from './components/UserAssetsSync';
*/

export function SwapScreen() {
const { AnimatedSwapStyles } = useSwapContext();
return (
<SwapSheetGestureBlocker>
<Box as={Page} style={styles.rootViewBackground} testID="swap-screen" width="full">
<SwapBackground />
<Box alignItems="center" height="full" paddingTop={{ custom: safeAreaInsetValues.top + (navbarHeight - 12) + 29 }} width="full">
<SwapInputAsset />
<FlipButton />
<SwapOutputAsset />
<Box as={Animated.View} width="full" position="absolute" bottom="0px" style={AnimatedSwapStyles.hideWhenInputsExpanded}>
<SliderAndKeyboard />
<SwapBottomPanel />
</Box>
<Box
as={Animated.View}
alignItems="center"
justifyContent="center"
style={[styles.swapWarningAndExchangeWrapper, AnimatedSwapStyles.hideWhileReviewingOrConfiguringGas]}
>
<ExchangeRateBubble />
<SwapWarning />
<SwapProvider>
<MountAndUnmountHandlers />
<SwapSheetGestureBlocker>
<Box as={Page} style={styles.rootViewBackground} testID="swap-screen" width="full">
<SwapBackground />
<Box alignItems="center" height="full" paddingTop={{ custom: safeAreaInsetValues.top + (navbarHeight - 12) + 29 }} width="full">
<SwapInputAsset />
<FlipButton />
<SwapOutputAsset />
<SliderAndKeyboardAndBottomControls />
<ExchangeRateBubbleAndWarning />
</Box>
<SwapNavbar />
</Box>
<SwapNavbar />

{/* NOTE: The components below render null and are solely for keeping react-query and Zustand in sync */}
<UserAssetsSync />
</Box>
</SwapSheetGestureBlocker>
</SwapSheetGestureBlocker>
<WalletAddressObserver />
</SwapProvider>
);
}

const MountAndUnmountHandlers = () => {
useMountSignal();
useCleanupOnUnmount();

return null;
};

const useMountSignal = () => {
useEffect(() => {
useSwapsStore.setState({ isSwapsOpen: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

const useCleanupOnUnmount = () => {
useEffect(() => {
return () => {
const highestValueAsset = userAssetsStore.getState().getHighestValueAsset();
const parsedAsset = highestValueAsset
? parseSearchAsset({
assetWithPrice: undefined,
searchAsset: highestValueAsset,
userAsset: highestValueAsset,
})
: null;

useSwapsStore.setState({
inputAsset: parsedAsset,
isSwapsOpen: false,
outputAsset: null,
outputSearchQuery: '',
quote: null,
selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet,
});

userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' });
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

const WalletAddressObserver = () => {
const currentWalletAddress = userAssetsStore(state => state.associatedWalletAddress);
const { setAsset } = useSwapContext();

const setNewInputAsset = useCallback(() => {
const newHighestValueAsset = userAssetsStore.getState().getHighestValueAsset();

if (userAssetsStore.getState().filter !== 'all') {
userAssetsStore.setState({ filter: 'all' });
}

setAsset({
type: SwapAssetType.inputAsset,
asset: newHighestValueAsset,
});
}, [setAsset]);

useAnimatedReaction(
() => currentWalletAddress,
(current, previous) => {
const didWalletAddressChange = previous && current !== previous;

if (didWalletAddressChange) {
runOnJS(setNewInputAsset)();
}
}
);

return null;
};

const SliderAndKeyboardAndBottomControls = () => {
const shouldMount = useDelayedMount();
const { AnimatedSwapStyles } = useSwapContext();

return shouldMount ? (
<Box as={Animated.View} width="full" position="absolute" bottom="0px" style={AnimatedSwapStyles.hideWhenInputsExpanded}>
<SliderAndKeyboard />
<SwapBottomPanel />
</Box>
) : null;
};

const ExchangeRateBubbleAndWarning = () => {
const { AnimatedSwapStyles } = useSwapContext();
return (
<Box
as={Animated.View}
alignItems="center"
justifyContent="center"
style={[styles.swapWarningAndExchangeWrapper, AnimatedSwapStyles.hideWhileReviewingOrConfiguringGas]}
>
<ExchangeRateBubble />
<SwapWarning />
</Box>
);
};

export const styles = StyleSheet.create({
rootViewBackground: {
borderRadius: IS_ANDROID ? 20 : ScreenCornerRadius,
Expand Down
9 changes: 5 additions & 4 deletions src/__swaps__/screens/Swap/components/AnimatedChainImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ export function AnimatedChainImage({
});

return (
<View style={[sx.badge, { bottom: 0, borderRadius: size / 2, width: size, height: size }]}>
{/* @ts-expect-error source prop is missing */}
<AnimatedFasterImage style={{ borderRadius: size / 2, width: size, height: size }} animatedProps={animatedIconSource} />
<View style={[sx.badge, { borderRadius: size / 2, height: size, width: size }]}>
{/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */}
{/* @ts-expect-error: Doesn't pick up that it's getting a source prop via animatedProps */}
<AnimatedFasterImage style={{ height: size, width: size }} animatedProps={animatedIconSource} />
</View>
);
}
Expand All @@ -118,7 +119,7 @@ const sx = StyleSheet.create({
height: 4,
width: 0,
},
shadowRadius: 6,
shadowOpacity: 0.2,
shadowRadius: 6,
},
});
59 changes: 44 additions & 15 deletions src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AnimatedFasterImage } from '@/components/AnimatedComponents/AnimatedFas
import { AnimatedChainImage } from './AnimatedChainImage';
import { fadeConfig } from '../constants';
import { SwapCoinIconTextFallback } from './SwapCoinIconTextFallback';
import { Box } from '@/design-system';

const fallbackIconStyle = {
...borders.buildCircleAsObject(32),
Expand All @@ -26,9 +27,9 @@ const smallFallbackIconStyle = {
position: 'absolute' as ViewStyle['position'],
};

export const AmimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
export const AnimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
asset,
large,
large = true,
small,
showBadge = true,
}: {
Expand All @@ -39,13 +40,13 @@ export const AmimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
}) {
const { isDarkMode, colors } = useTheme();

const imageLoadingError = useSharedValue(false);
const didErrorForUniqueId = useSharedValue<string | undefined>(undefined);

const animatedIconSource = useAnimatedProps(() => {
return {
source: {
...DEFAULT_FASTER_IMAGE_CONFIG,
borderRadius: (small ? 16 : large ? 36 : 32) / 2,
Copy link
Contributor

@walmat walmat May 30, 2024

Choose a reason for hiding this comment

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

transitionDuration: 0,
url: asset.value?.icon_url ?? '',
},
};
Expand All @@ -58,17 +59,29 @@ export const AmimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
});

const animatedCoinIconStyles = useAnimatedStyle(() => {
const showFallback = imageLoadingError.value || !asset.value?.icon_url;
const showEmptyState = !asset.value?.uniqueId;
const showFallback = didErrorForUniqueId.value === asset.value?.uniqueId;
const shouldDisplay = !showFallback && !showEmptyState;

return {
display: showFallback ? 'none' : 'flex',
pointerEvents: showFallback ? 'none' : 'auto',
opacity: withTiming(showFallback ? 0 : 1, fadeConfig),
display: shouldDisplay ? 'flex' : 'none',
pointerEvents: shouldDisplay ? 'auto' : 'none',
opacity: withTiming(shouldDisplay ? 1 : 0, fadeConfig),
};
});

const animatedEmptyStateStyles = useAnimatedStyle(() => {
const showEmptyState = !asset.value?.uniqueId;

return {
display: showEmptyState ? 'flex' : 'none',
opacity: withTiming(showEmptyState ? 1 : 0, fadeConfig),
};
});

const animatedFallbackStyles = useAnimatedStyle(() => {
const showFallback = imageLoadingError.value || !asset.value?.icon_url;
const showEmptyState = !asset.value?.uniqueId;
const showFallback = !showEmptyState && didErrorForUniqueId.value === asset.value?.uniqueId;

return {
display: showFallback ? 'flex' : 'none',
Expand All @@ -88,23 +101,22 @@ export const AmimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
]}
>
<Animated.View style={animatedCoinIconStyles}>
{/* @ts-expect-error missing props "source" */}
{/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */}
{/* @ts-expect-error: Doesn't pick up that it's getting a source prop via animatedProps */}
<AnimatedFasterImage
animatedProps={animatedIconSource}
onError={() => {
'worklet';
imageLoadingError.value = true;
didErrorForUniqueId.value = asset.value?.uniqueId;
}}
onSuccess={() => {
'worklet';
imageLoadingError.value = false;
didErrorForUniqueId.value = undefined;
}}
style={[
sx.coinIcon,
{
borderRadius: (small ? 16 : large ? 36 : 32) / 2,
height: small ? 16 : large ? 36 : 32,
width: small ? 16 : large ? 36 : 32,
borderRadius: (small ? 16 : large ? 36 : 32) / 2,
},
]}
/>
Expand All @@ -120,6 +132,20 @@ export const AmimatedSwapCoinIcon = React.memo(function FeedCoinIcon({
style={small ? smallFallbackIconStyle : large ? largeFallbackIconStyle : fallbackIconStyle}
/>
</Animated.View>

<Box
as={Animated.View}
background="fillQuaternary"
style={[
animatedEmptyStateStyles,
small ? sx.coinIconFallbackSmall : large ? sx.coinIconFallbackLarge : sx.coinIconFallback,
{
borderRadius: (small ? 16 : large ? 36 : 32) / 2,
height: small ? 16 : large ? 36 : 32,
width: small ? 16 : large ? 36 : 32,
},
]}
/>
</Animated.View>

{showBadge && <AnimatedChainImage asset={asset} size={16} />}
Expand Down Expand Up @@ -164,6 +190,9 @@ const sx = StyleSheet.create({
height: 16,
overflow: 'visible',
},
emptyState: {
pointerEvents: 'none',
},
reactCoinIconContainer: {
position: 'relative',
alignItems: 'center',
Expand Down
13 changes: 7 additions & 6 deletions src/__swaps__/screens/Swap/components/AnimatedSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';

import { AnimatedText, Box, Inline, globalColors, useColorMode, useForegroundColor } from '@/design-system';
import Animated, { SharedValue, useAnimatedStyle, useDerivedValue, withSpring, withTiming } from 'react-native-reanimated';
import { fadeConfig, springConfig } from '../constants';
import { opacityWorklet } from '@/__swaps__/utils/swaps';
import { GestureHandlerButtonProps, GestureHandlerV1Button } from './GestureHandlerV1Button';
import { StyleSheet } from 'react-native';
import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs';

type AnimatedSwitchProps = {
onToggle: () => void;
Expand All @@ -25,8 +24,8 @@ export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, ..
const containerStyles = useAnimatedStyle(() => {
return {
backgroundColor: !value.value
? withTiming(opacityWorklet(inactiveBg, 0.12), fadeConfig)
: withTiming(opacityWorklet(activeBg, 0.64), fadeConfig),
? withTiming(opacityWorklet(inactiveBg, 0.12), TIMING_CONFIGS.fadeConfig)
: withTiming(opacityWorklet(activeBg, 0.64), TIMING_CONFIGS.fadeConfig),
borderColor: opacityWorklet(border, 0.06),
};
});
Expand All @@ -35,7 +34,7 @@ export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, ..
return {
transform: [
{
translateX: withSpring(value.value ? 11 : 1, springConfig),
translateX: withSpring(value.value ? 11 : 1, SPRING_CONFIGS.springConfig),
},
],
};
Expand All @@ -57,7 +56,9 @@ export function AnimatedSwitch({ value, onToggle, activeLabel, inactiveLabel, ..
return (
<GestureHandlerV1Button onPressWorklet={onToggle} {...props}>
<Inline alignVertical="center" horizontalSpace="6px">
<AnimatedText align="right" color={isDarkMode ? 'labelSecondary' : 'label'} size="15pt" weight="heavy" text={labelItem} />
<AnimatedText align="right" color={isDarkMode ? 'labelSecondary' : 'label'} size="15pt" weight="heavy">
{labelItem}
</AnimatedText>
<Box as={Animated.View} style={[styles.containerStyles, containerStyles]}>
<Box style={[styles.circleStyles, circleStyles]} as={Animated.View} />
</Box>
Expand Down
Loading
Loading