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

swap button states #5785

Merged
merged 38 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
46162ee
wip
greg-schrammel May 29, 2024
3a3c341
fix gesture button states
walmat May 29, 2024
b8635be
Safemath pt 2 (#5778)
brunobar79 May 29, 2024
251f247
fix dynamic island overlap on recieve modal (#5672)
BrodyHughes May 29, 2024
ecb04e1
Gas optimizations (#5779)
greg-schrammel May 29, 2024
7e367e1
fix other networks section (#5784)
walmat May 29, 2024
f3307cb
Swaps: fix favorite button press (#5782)
benisgold May 29, 2024
009f887
todo
greg-schrammel May 29, 2024
6fd56f7
Merge remote-tracking branch 'origin/develop' into review-button-states
greg-schrammel May 29, 2024
2ce9e5f
remove console logs
greg-schrammel May 29, 2024
3c86b3b
Merge branch 'develop' into review-button-states
greg-schrammel May 30, 2024
a5799fb
Insufficient Funds
greg-schrammel May 30, 2024
9d816a8
remove todo
greg-schrammel May 30, 2024
a17464e
move cache getter closer to fetcher implentation
greg-schrammel Jun 2, 2024
c60dc66
Merge remote-tracking branch 'origin/develop' into review-button-states
greg-schrammel Jun 3, 2024
93cf94c
Merge branch 'develop' into review-button-states
greg-schrammel Jun 4, 2024
0af8ff4
fix
greg-schrammel Jun 5, 2024
0187af5
Merge remote-tracking branch 'origin/develop' into review-button-states
greg-schrammel Jun 5, 2024
40c2055
:)
greg-schrammel Jun 5, 2024
adf3b51
Merge branch 'develop' into review-button-states
walmat Jun 7, 2024
c7c0102
🍕
greg-schrammel Jun 12, 2024
cba4aed
or equal 🤌
greg-schrammel Jun 12, 2024
2ea366e
remove unused isSameAddress util
greg-schrammel Jun 12, 2024
e6d4382
just reordering declarations
greg-schrammel Jun 12, 2024
31d36b6
error i18n
greg-schrammel Jun 12, 2024
508a13b
fix merge conflicts with import sorting
walmat Jun 13, 2024
76df24c
useGasSharedValues
greg-schrammel Jun 13, 2024
f93b7ad
remove estimating
greg-schrammel Jun 13, 2024
c9bd12f
Merge branch 'develop' into review-button-states
greg-schrammel Jun 13, 2024
23c29c8
Merge branch 'develop' into review-button-states
greg-schrammel Jun 13, 2024
606866f
Merge branch 'develop' into review-button-states
walmat Jun 13, 2024
7697803
fix label flickering
greg-schrammel Jun 14, 2024
da8d964
fix review panel not prompting
walmat Jun 14, 2024
2819c72
Merge branch 'develop' into review-button-states
walmat Jun 14, 2024
fea8f65
Revert "Lint on pre-commit (#5836)"
walmat Jun 14, 2024
955e223
fix a bunch of shit
walmat Jun 14, 2024
8cfad78
Merge branch 'develop' of https://github.com/rainbow-me/rainbow into …
walmat Jun 14, 2024
988541d
Merge branch 'develop' into review-button-states
walmat Jun 14, 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
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>;
Expand Down
32 changes: 22 additions & 10 deletions src/__swaps__/screens/Swap/components/SwapActionButton.tsx
Original file line number Diff line number Diff line change
@@ -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 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,
Expand All @@ -23,6 +25,8 @@ export const SwapActionButton = ({
scaleTo,
small,
style,
disabled,
isLoading,
}: {
asset: DerivedValue<ExtendedAnimatedAssetWithColors | null>;
borderRadius?: number;
Expand All @@ -38,6 +42,8 @@ export const SwapActionButton = ({
scaleTo?: number;
small?: boolean;
style?: ViewStyle;
disabled?: DerivedValue<boolean | undefined>;
isLoading?: DerivedValue<boolean | undefined>;
}) => {
const { isDarkMode } = useColorMode();
const fallbackColor = useForegroundColor('label');
Expand Down Expand Up @@ -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,
};
});

Expand All @@ -97,15 +105,19 @@ export const SwapActionButton = ({
return rightIcon;
});

const buttonAnimatedProps = useAnimatedProps(() => {
return {
disabled: disabled?.value,
scaleTo: disabled?.value ? 1 : scaleTo || (hugContent ? undefined : 0.925),
};
});

return (
<GestureHandlerV1Button
<AnimatedGestureHandlerV1Button
animatedProps={buttonAnimatedProps}
onPressStartWorklet={onPressWorklet}
onPressJS={onPressJS}
onPressWorklet={onPressWorklet}
scaleTo={scaleTo || (hugContent ? undefined : 0.925)}
style={{
...(hugContent && feedActionButtonStyles.buttonWrapper),
...(style || {}),
}}
style={[hugContent && feedActionButtonStyles.buttonWrapper, style]}
>
<Box
as={Animated.View}
Expand Down Expand Up @@ -138,7 +150,7 @@ export const SwapActionButton = ({
)}
</Columns>
</Box>
</GestureHandlerV1Button>
</AnimatedGestureHandlerV1Button>
);
};

Expand Down
30 changes: 15 additions & 15 deletions src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import React from 'react';
import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated';
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 { Box, Column, Columns, Separator, globalColors, useColorMode } from '@/design-system';
import { safeAreaInsetValues } from '@/utils';

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 { IS_ANDROID } from '@/env';
import { useSwapContext, NavigationSteps } from '@/__swaps__/screens/Swap/providers/swap-provider';

import { opacity } from '@/__swaps__/utils/swaps';
import { SPRING_CONFIGS } from '@/components/animations/animationConfigs';
import { useBottomPanelGestureHandler } from '../hooks/useBottomPanelGestureHandler';
import { GasButton } from './GasButton';
import { GasPanel } from './GasPanel';
import { ReviewPanel } from './ReviewPanel';
import { SwapActionButton } from './SwapActionButton';
import { SPRING_CONFIGS } from '@/components/animations/animationConfigs';

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();

Expand All @@ -50,6 +43,11 @@ export function SwapBottomPanel() {
};
});

const icon = useDerivedValue(() => confirmButtonProps.value.icon);
const label = useDerivedValue(() => confirmButtonProps.value.label);
const disabled = useDerivedValue(() => confirmButtonProps.value.disabled);
const isLoading = useDerivedValue(() => confirmButtonProps.value.isLoading);

return (
// @ts-expect-error Property 'children' does not exist on type
<PanGestureHandler maxPointers={1} onGestureEvent={swipeToDismissGestureHandler}>
Expand Down Expand Up @@ -80,11 +78,13 @@ export function SwapBottomPanel() {
</Box>
</Column>
<SwapActionButton
onPressWorklet={SwapNavigation.handleSwapAction}
asset={internalSelectedOutputAsset}
icon={confirmButtonIcon}
icon={icon}
iconStyle={confirmButtonIconStyle}
label={confirmButtonLabel}
onPressWorklet={SwapNavigation.handleSwapAction}
label={label}
disabled={disabled}
isLoading={isLoading}
scaleTo={0.9}
/>
</Columns>
Expand Down
8 changes: 4 additions & 4 deletions src/__swaps__/screens/Swap/components/SwapInputAsset.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
17 changes: 9 additions & 8 deletions src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
32 changes: 32 additions & 0 deletions src/__swaps__/screens/Swap/hooks/useCanPerformSwap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { lessThanOrEqualToWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath';
import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets';
import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps';
import { SharedValue, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import { GasSettings } from './useCustomGas';

export function useCanPerformSwap({
quote,
inputAsset,
}: {
inputAsset: SharedValue<ExtendedAnimatedAssetWithColors | null>;
quote: SharedValue<Quote | CrosschainQuote | QuoteError | null>;
}) {
const gasSettings = useSharedValue<GasSettings | undefined>(undefined);
const estimatedGasLimit = useSharedValue<string | undefined>(undefined);
const enoughFundsForGas = useSharedValue<boolean>(true);

const enoughFundsForSwap = useDerivedValue(() => {
if (!quote.value || 'error' in quote.value || !inputAsset.value) return true;
walmat marked this conversation as resolved.
Show resolved Hide resolved
return lessThanOrEqualToWorklet(
quote.value.sellAmount.toString(),
toScaledIntegerWorklet(inputAsset.value.balance.amount, inputAsset.value.decimals)
);
});

return {
gasSettings,
estimatedGasLimit,
enoughFundsForGas,
enoughFundsForSwap,
};
}
70 changes: 15 additions & 55 deletions src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import { ChainId } from '@/__swaps__/types/chains';
import { weiToGwei } from '@/__swaps__/utils/ethereum';
import { add, multiply } from '@/__swaps__/utils/numbers';
import ethereumUtils, { useNativeAssetForNetwork } from '@/utils/ethereumUtils';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { formatUnits } from 'viem';

import { useSyncedSwapQuoteStore } from '../providers/SyncSwapStateAndSharedValues';
import { formatCurrency, formatNumber } from './formatNumber';
import { GasSettings } from './useCustomGas';
import { useSwapEstimatedGasLimit } from './useSwapEstimatedGasLimit';
import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { useDebouncedCallback } from 'use-debounce';
import { useSwapContext } from '../providers/swap-provider';
import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath';

function safeBigInt(value: string) {
try {
Expand All @@ -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,
Expand All @@ -35,66 +38,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 formatCurrency(feeInUserCurrency);
}, [gasLimit, gasSettings, 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]);
}
Loading