Skip to content

Commit

Permalink
<UnmountOnAnimatedReaction /> (#5810)
Browse files Browse the repository at this point in the history
* mount/unmount panels based on visibility

* Stall

* fix merge

* better name

* remove the based

* UnmountOnAnimatedReaction

* lint

---------

Co-authored-by: Matthew Wall <[email protected]>
  • Loading branch information
greg-schrammel and walmat authored Jun 13, 2024
1 parent 1d2884c commit adde11f
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 65 deletions.
28 changes: 15 additions & 13 deletions src/__swaps__/screens/Swap/components/EstimatedSwapGasFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@ import { AnimatedText, TextProps, useForegroundColor } from '@/design-system';
import React, { memo } from 'react';
import { SharedValue, useAnimatedStyle, useDerivedValue, withRepeat, withSequence, withSpring, withTiming } from 'react-native-reanimated';

import { opacity } from '@/__swaps__/utils/swaps';
import { TIMING_CONFIGS } from '@/components/animations/animationConfigs';
import { useDelayedValue } from '@/hooks/reanimated/useDelayedValue';
import { pulsingConfig, sliderConfig } from '../constants';
import { GasSettings } from '../hooks/useCustomGas';
import { useSwapEstimatedGasFee } from '../hooks/useEstimatedGasFee';
import { useSwapContext } from '../providers/swap-provider';
import { opacity } from '@/__swaps__/utils/swaps';
import { TIMING_CONFIGS } from '@/components/animations/animationConfigs';
import { useDelayedValue } from '@/hooks/reanimated/useDelayedValue';

export const EstimatedSwapGasFee = memo(function EstimatedSwapGasFee({
gasSettings,
align,
type EstimatedSwapGasFeeProps = { gasSettings: GasSettings | undefined } & Partial<
Pick<TextProps, 'align' | 'color' | 'size' | 'weight' | 'tabularNumbers'>
>;
export function EstimatedSwapGasFeeSlot({
color = 'labelTertiary',
size = '15pt',
weight = 'bold',
tabularNumbers = true,
}: { gasSettings: GasSettings | undefined } & Partial<Pick<TextProps, 'align' | 'color' | 'size' | 'weight' | 'tabularNumbers'>>) {
...props
}: { text: string } & Omit<EstimatedSwapGasFeeProps, 'gasSettings'>) {
const label = useDerivedValue(() => props.text);
return <GasFeeText color={color} size={size} weight={weight} {...props} label={label} />;
}
export function EstimatedSwapGasFee({ gasSettings, ...props }: EstimatedSwapGasFeeProps) {
const { data: estimatedGasFee = '--' } = useSwapEstimatedGasFee(gasSettings);

const label = useDerivedValue(() => estimatedGasFee);

return <GasFeeText align={align} color={color} size={size} weight={weight} tabularNumbers={tabularNumbers} label={label} />;
});
return <EstimatedSwapGasFeeSlot {...props} text={estimatedGasFee} />;
}

const GasFeeText = memo(function GasFeeText({
align,
Expand Down
33 changes: 26 additions & 7 deletions src/__swaps__/screens/Swap/components/GasButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ChainId } from '@/__swaps__/types/chains';
import { GasSpeed } from '@/__swaps__/types/gas';
import { weiToGwei } from '@/__swaps__/utils/ethereum';
import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu';
import { getCachedCurrentBaseFee, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology';
import { add } from '@/__swaps__/utils/numbers';
import { ButtonPressAnimation } from '@/components/animations';
import { ContextMenu } from '@/components/context-menu';
import { Centered } from '@/components/layout';
import ContextMenuButton from '@/components/native-context-menu/contextMenu';
Expand All @@ -11,22 +12,38 @@ import { IS_ANDROID } from '@/env';
import * as i18n from '@/languages';
import { swapsStore } from '@/state/swaps/swapsStore';
import { gasUtils } from '@/utils';
import React, { ReactNode, useCallback, useMemo } from 'react';
import React, { PropsWithChildren, ReactNode, useCallback, useMemo } from 'react';
import { StyleSheet } from 'react-native';
import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu';
import { runOnJS, runOnUI } from 'react-native-reanimated';
import { ETH_COLOR, ETH_COLOR_DARK, THICK_BORDER_WIDTH } from '../constants';
import { formatNumber } from '../hooks/formatNumber';
import { GasSettings, useCustomGasSettings } from '../hooks/useCustomGas';
import { GasSpeed } from '@/__swaps__/types/gas';
import { setSelectedGasSpeed, useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { useSwapContext } from '../providers/swap-provider';
import { EstimatedSwapGasFee } from './EstimatedSwapGasFee';
import { NavigationSteps, useSwapContext } from '../providers/swap-provider';
import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee';
import { GestureHandlerV1Button } from './GestureHandlerV1Button';
import { ButtonPressAnimation } from '@/components/animations';
import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction';

const { GAS_ICONS } = gasUtils;
const GAS_BUTTON_HIT_SLOP = 16;

function UnmountWhenGasButtonIsNotInScreen({ placeholder, children }: PropsWithChildren<{ placeholder: ReactNode }>) {
const { configProgress } = useSwapContext();
return (
<UnmountOnAnimatedReaction
isMountedWorklet={() => {
'worklet';
// unmount when custom gas or review panels are above it
return !(configProgress.value === NavigationSteps.SHOW_GAS || configProgress.value === NavigationSteps.SHOW_REVIEW);
}}
placeholder={placeholder}
>
{children}
</UnmountOnAnimatedReaction>
);
}

function EstimatedGasFee() {
const chainId = swapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const gasSettings = useSelectedGas(chainId);
Expand All @@ -36,7 +53,9 @@ function EstimatedGasFee() {
<TextIcon color="labelQuaternary" height={10} size="icon 11px" weight="heavy" width={16}>
􀵟
</TextIcon>
<EstimatedSwapGasFee gasSettings={gasSettings} />
<UnmountWhenGasButtonIsNotInScreen placeholder={<EstimatedSwapGasFeeSlot text="--" />}>
<EstimatedSwapGasFee gasSettings={gasSettings} />
</UnmountWhenGasButtonIsNotInScreen>
</Inline>
);
}
Expand Down
84 changes: 55 additions & 29 deletions src/__swaps__/screens/Swap/components/GasPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as i18n from '@/languages';
import React, { PropsWithChildren, useMemo } from 'react';
import React, { PropsWithChildren, ReactNode, useMemo } from 'react';
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';

import { fadeConfig } from '@/__swaps__/screens/Swap/constants';
import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { ChainId } from '@/__swaps__/types/chains';
import { GasSpeed } from '@/__swaps__/types/gas';
import { gweiToWei, weiToGwei } from '@/__swaps__/utils/ethereum';
import {
getSelectedSpeedSuggestion,
Expand All @@ -23,13 +24,13 @@ import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { upperFirst } from 'lodash';
import { gasUtils } from '@/utils';
import { upperFirst } from 'lodash';
import { formatNumber } from '../hooks/formatNumber';
import { GasSettings, getCustomGasSettings, setCustomGasSettings, useCustomGasStore } from '../hooks/useCustomGas';
import { setSelectedGasSpeed, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { EstimatedSwapGasFee } from './EstimatedSwapGasFee';
import { GasSpeed } from '@/__swaps__/types/gas';
import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee';
import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction';

const { GAS_TRENDS } = gasUtils;

Expand All @@ -43,6 +44,22 @@ type AlertInfo = {
message: string;
} | null;

function UnmountWhenGasPanelIsClosed({ placeholder, children }: PropsWithChildren<{ placeholder: ReactNode }>) {
const { configProgress } = useSwapContext();
return (
<UnmountOnAnimatedReaction
isMountedWorklet={() => {
'worklet';
// only mounted when custom gas panel is open
return configProgress.value === NavigationSteps.SHOW_GAS;
}}
placeholder={placeholder}
>
{children}
</UnmountOnAnimatedReaction>
);
}

function PressableLabel({ onPress, children }: PropsWithChildren<{ onPress: VoidFunction }>) {
return (
<Box
Expand Down Expand Up @@ -136,37 +153,31 @@ function GasSettingInput({

const selectWeiToGwei = (s: string | undefined) => s && weiToGwei(s);

function CurrentBaseFee() {
function CurrentBaseFeeSlot({ baseFee, gasTrend = 'notrend' }: { baseFee?: string; gasTrend?: keyof typeof GAS_TRENDS }) {
const { isDarkMode } = useColorMode();
const { navigate } = useNavigation();

const label = useForegroundColor('label');
const labelSecondary = useForegroundColor('labelSecondary');

const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const { data: baseFee } = useBaseFee({ chainId, select: selectWeiToGwei });
const { data: gasTrend = 'notrend' } = useGasTrend({ chainId });

const trendType = 'currentBaseFee' + upperFirst(gasTrend);

// loading state?

const isEIP1559 = useIsChainEIP1559(chainId);
if (!isEIP1559) return null;

const onPressLabel = () => {
if (!baseFee || !gasTrend) return;
navigate(Routes.EXPLAIN_SHEET, {
currentBaseFee: baseFee,
currentGasTrend: gasTrend,
type: trendType,
});
};

return (
<Inline horizontalSpace="10px" alignVertical="center" alignHorizontal="justify">
<PressableLabel
onPress={() =>
navigate(Routes.EXPLAIN_SHEET, {
currentBaseFee: baseFee,
currentGasTrend: gasTrend,
type: trendType,
})
}
>
{i18n.t(i18n.l.gas.current_base_fee)}
</PressableLabel>
<PressableLabel onPress={onPressLabel}>{i18n.t(i18n.l.gas.current_base_fee)}</PressableLabel>
<Bleed top="16px">
<Stack space="8px">
<Text
Expand Down Expand Up @@ -195,6 +206,13 @@ function CurrentBaseFee() {
);
}

function CurrentBaseFee() {
const chainId = useSwapsStore(s => s.inputAsset?.chainId || ChainId.mainnet);
const { data: baseFee } = useBaseFee({ chainId, select: selectWeiToGwei });
const { data: gasTrend } = useGasTrend({ chainId });
return <CurrentBaseFeeSlot baseFee={baseFee} gasTrend={gasTrend} />;
}

type GasPanelState = { gasPrice?: string; maxBaseFee?: string; maxPriorityFee?: string };
const useGasPanelStore = createRainbowStore<GasPanelState | undefined>(() => undefined);

Expand Down Expand Up @@ -303,13 +321,19 @@ function MaxTransactionFee() {
</Inline>

<Inline horizontalSpace="6px">
<EstimatedSwapGasFee
gasSettings={gasSettings}
align="right"
color={isDarkMode ? 'labelSecondary' : 'label'}
size="15pt"
weight="heavy"
/>
<UnmountWhenGasPanelIsClosed
placeholder={
<EstimatedSwapGasFeeSlot align="right" color={isDarkMode ? 'labelSecondary' : 'label'} size="15pt" weight="heavy" text="--" />
}
>
<EstimatedSwapGasFee
gasSettings={gasSettings}
align="right"
color={isDarkMode ? 'labelSecondary' : 'label'}
size="15pt"
weight="heavy"
/>
</UnmountWhenGasPanelIsClosed>
</Inline>
</Inline>
);
Expand Down Expand Up @@ -368,7 +392,9 @@ export function GasPanel() {
</Text>

<Box gap={24} width="full" alignItems="stretch">
<CurrentBaseFee />
<UnmountWhenGasPanelIsClosed placeholder={<CurrentBaseFeeSlot />}>
<CurrentBaseFee />
</UnmountWhenGasPanelIsClosed>

<EditableGasSettings />

Expand Down
33 changes: 24 additions & 9 deletions src/__swaps__/screens/Swap/components/ReviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps';
import { AnimatedChainImage } from '@/__swaps__/screens/Swap/components/AnimatedChainImage';
import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button';
import { useNativeAssetForChain } from '@/__swaps__/screens/Swap/hooks/useNativeAssetForChain';
import { chainNameFromChainId } from '@/__swaps__/utils/chains';
import { useEstimatedTime } from '@/__swaps__/utils/meteorology';
import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, handleSignificantDecimals, multiply } from '@/__swaps__/utils/numbers';
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
import { useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { EstimatedSwapGasFee } from './EstimatedSwapGasFee';
import { ButtonPressAnimation } from '@/components/animations';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { getNetworkObj } from '@/networks';
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
import { ethereumUtils } from '@/utils';
import { getNativeAssetForNetwork } from '@/utils/ethereumUtils';
import { getNetworkObj } from '@/networks';
import { chainNameFromChainId } from '@/__swaps__/utils/chains';
import { useSelectedGas, useSelectedGasSpeed } from '../hooks/useSelectedGas';
import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee';
import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction';

const UNKNOWN_LABEL = i18n.t(i18n.l.swap.unknown);
const REVIEW_LABEL = i18n.t(i18n.l.expanded_state.swap_details.review);
Expand Down Expand Up @@ -375,10 +376,24 @@ export function ReviewPanel() {
<View style={sx.gasContainer}>
<AnimatedChainImage showMainnetBadge asset={internalSelectedInputAsset} size={16} />
</View>
<Inline horizontalSpace="4px">
<EstimatedGasFee />
<EstimatedArrivalTime />
</Inline>
<UnmountOnAnimatedReaction
isMountedWorklet={() => {
'worklet';
// only mounted when review panel is visible
return configProgress.value === NavigationSteps.SHOW_REVIEW;
}}
placeholder={
<Inline horizontalSpace="4px">
<EstimatedSwapGasFeeSlot text="--" align="left" color="label" size="15pt" weight="heavy" />
{null}
</Inline>
}
>
<Inline horizontalSpace="4px">
<EstimatedGasFee />
<EstimatedArrivalTime />
</Inline>
</UnmountOnAnimatedReaction>
</Inline>

<Inline wrap={false} alignHorizontal="left" alignVertical="bottom" horizontalSpace="4px">
Expand Down
6 changes: 3 additions & 3 deletions src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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, 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PropsWithChildren, ReactNode, useState } from 'react';
import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';

export function UnmountOnAnimatedReaction({
isMountedWorklet,
placeholder,
children,
}: PropsWithChildren<{ placeholder: ReactNode; isMountedWorklet: () => boolean }>) {
const [isMounted, setIsMounted] = useState(false);

useAnimatedReaction(isMountedWorklet, v => runOnJS(setIsMounted)(v));

if (isMounted) return children;
return placeholder;
}
8 changes: 4 additions & 4 deletions src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { greaterThanWorklet, toScaledIntegerWorklet } from '@/__swaps__/safe-math/SafeMath';
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 { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { useDebouncedCallback } from 'use-debounce';
import { formatUnits } from 'viem';
import { useSwapContext } from '../providers/swap-provider';
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 Down
1 change: 1 addition & 0 deletions src/__swaps__/utils/meteorology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ function selectGasTrend({ data }: MeteorologyResult) {
if (trend === 0) return 'stable';
return 'notrend';
}
export type GasTrend = ReturnType<typeof selectGasTrend>;

export function useGasTrend({ chainId }: { chainId: ChainId }) {
return useMeteorology({ chainId }, { select: selectGasTrend });
Expand Down

0 comments on commit adde11f

Please sign in to comment.