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

Price Impact Warning #5635

Merged
merged 12 commits into from
Apr 18, 2024
2 changes: 2 additions & 0 deletions src/__swaps__/screens/Swap/Swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SwapInputAsset } from '@/__swaps__/screens/Swap/components/controls/Swa
import { SwapOutputAsset } from '@/__swaps__/screens/Swap/components/controls/SwapOutputAsset';
import { SwapNavbar } from '@/__swaps__/screens/Swap/components/SwapNavbar';
import { SwapAmountInputs } from '@/__swaps__/screens/Swap/components/controls/SwapAmountInputs';
import { PriceImpactWarning } from './components/PriceImpactWarning';

/** README
* This prototype is largely driven by Reanimated and Gesture Handler, which
Expand Down Expand Up @@ -64,6 +65,7 @@ export function SwapScreen() {
<FlipButton />
<SwapOutputAsset />
<ExchangeRateBubble />
<PriceImpactWarning />
<SwapAmountInputs />
</Box>
<SwapNavbar />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export const ExchangeRateBubble = () => {
justifyContent="center"
paddingHorizontal="24px"
paddingVertical="12px"
style={[AnimatedSwapStyles.hideWhenInputsExpanded, { alignSelf: 'center' }]}
style={[AnimatedSwapStyles.hideWhenInputsExpanded, AnimatedSwapStyles.hideWhenPriceImpactWarningIsPresent, { alignSelf: 'center' }]}
>
<Box
as={Animated.View}
Expand Down
53 changes: 53 additions & 0 deletions src/__swaps__/screens/Swap/components/PriceImpactWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import * as i18n from '@/languages';
import Animated, { useDerivedValue } from 'react-native-reanimated';
import { AnimatedText, Box, Inline, Text, TextIcon, useForegroundColor } from '@/design-system';
import { opacity } from '@/__swaps__/utils/swaps';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { SwapPriceImpactType } from '@/hooks/usePriceImpactDetails';

export const PriceImpactWarning = () => {
const { AnimatedSwapStyles, PriceImpactWarning } = useSwapContext();

// TODO: i18n is not liking this...
const warningText = useDerivedValue(() => {
if (PriceImpactWarning.value.type === SwapPriceImpactType.none) return '';
return i18n.t(i18n.l.exchange.price_impact.you_are_losing, {
impactDisplay: PriceImpactWarning.value.impactDisplay,
});
});
walmat marked this conversation as resolved.
Show resolved Hide resolved

const fillTertiary = useForegroundColor('fillTertiary');

return (
<Box
as={Animated.View}
alignItems="center"
justifyContent="center"
paddingHorizontal="24px"
paddingVertical="12px"
style={[AnimatedSwapStyles.hideWhenInputsExpanded, AnimatedSwapStyles.hideWhenPriceWarningIsNotPresent, { alignSelf: 'center' }]}
>
<Box as={Animated.View} alignItems="center" height={{ custom: 33 }} gap={6} justifyContent="center" paddingHorizontal="10px">
<Inline alignHorizontal="center" alignVertical="center" horizontalSpace="4px" wrap={false}>
<Box
borderRadius={10}
height={{ custom: 20 }}
paddingTop={{ custom: 0.25 }}
style={{ backgroundColor: opacity(fillTertiary, 0.04) }}
width={{ custom: 20 }}
>
<TextIcon color="orange" containerSize={20} size="15pt" weight="heavy">
􀇿
</TextIcon>
</Box>
<AnimatedText align="center" color="orange" size="15pt" weight="heavy" text={warningText} />
</Inline>

<Text color="labelQuaternary" size="13pt" weight="bold">
{i18n.t(i18n.l.exchange.price_impact.smal_market_try_smaller_amount)}
walmat marked this conversation as resolved.
Show resolved Hide resolved
</Text>
</Box>
</Box>
);
};
19 changes: 19 additions & 0 deletions src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import {
} from '@/__swaps__/screens/Swap/constants';
import { opacityWorklet } from '@/__swaps__/utils/swaps';
import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController';
import { SwapPriceImpactType, usePriceImpactWarning } from '@/__swaps__/screens/Swap/hooks/usePriceImpactWarning';
import { spinnerExitConfig } from '@/__swaps__/components/animations/AnimatedSpinner';

export function useAnimatedSwapStyles({
SwapInputController,
PriceImpactWarning,
inputProgress,
outputProgress,
isFetching,
}: {
SwapInputController: ReturnType<typeof useSwapInputsController>;
PriceImpactWarning: ReturnType<typeof usePriceImpactWarning>;
inputProgress: SharedValue<number>;
outputProgress: SharedValue<number>;
isFetching: SharedValue<boolean>;
Expand All @@ -45,6 +48,20 @@ export function useAnimatedSwapStyles({
};
});

const hideWhenPriceWarningIsNotPresent = useAnimatedStyle(() => {
return {
opacity: PriceImpactWarning.value?.type === SwapPriceImpactType.none ? withTiming(0, fadeConfig) : withTiming(1, fadeConfig),
pointerEvents: PriceImpactWarning.value?.type === SwapPriceImpactType.none ? 'none' : 'auto',
};
});

const hideWhenPriceImpactWarningIsPresent = useAnimatedStyle(() => {
return {
opacity: PriceImpactWarning.value?.type !== SwapPriceImpactType.none ? withTiming(0, fadeConfig) : withTiming(1, fadeConfig),
pointerEvents: PriceImpactWarning.value?.type !== SwapPriceImpactType.none ? 'none' : 'auto',
};
});

const hideWhenInputsExpanded = useAnimatedStyle(() => {
return {
opacity: inputProgress.value > 0 || outputProgress.value > 0 ? withTiming(0, fadeConfig) : withTiming(1, fadeConfig),
Expand Down Expand Up @@ -164,6 +181,8 @@ export function useAnimatedSwapStyles({
flipButtonStyle,
focusedSearchStyle,
hideWhenInputsExpanded,
hideWhenPriceImpactWarningIsPresent,
hideWhenPriceWarningIsNotPresent,
inputStyle,
inputTokenListStyle,
keyboardStyle,
Expand Down
102 changes: 102 additions & 0 deletions src/__swaps__/screens/Swap/hooks/usePriceImpactWarning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useCallback } from 'react';
import { useAccountSettings } from '@/hooks';
import { SharedValue, runOnJS, runOnUI, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
import { convertAmountToNativeDisplay, divide, greaterThanOrEqualTo, subtract } from '@/__swaps__/utils/numbers';
import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController';
import { BigNumberish } from '@/__swaps__/utils/hex';

const highPriceImpactThreshold = 0.05;
const severePriceImpactThreshold = 0.1;

export enum SwapPriceImpactType {
none = 'none',
high = 'high',
severe = 'severe',
}

export interface SwapPriceImpact {
type: SwapPriceImpactType;
impactDisplay: string;
}

type UsePriceImpactWarningProps = {
SwapInputController: ReturnType<typeof useSwapInputsController>;
isFetching: SharedValue<boolean>;
};

export const usePriceImpactWarning = ({ SwapInputController, isFetching }: UsePriceImpactWarningProps) => {
const { nativeCurrency: currentCurrency } = useAccountSettings();

const priceImpact = useSharedValue<SwapPriceImpact>({
type: SwapPriceImpactType.none,
impactDisplay: '',
});

const calculatePriceImpact = useCallback(
({
inputNativeValue,
outputNativeValue,
isFetching,
}: {
inputNativeValue: BigNumberish;
outputNativeValue: BigNumberish;
isFetching: boolean;
}) => {
const nativeAmountImpact = subtract(inputNativeValue, outputNativeValue);
const impact = divide(nativeAmountImpact, inputNativeValue);

const impactDisplay = convertAmountToNativeDisplay(nativeAmountImpact, currentCurrency);

const updatePriceImpact = (values: SwapPriceImpact) => {
'worklet';

priceImpact.modify(prev => ({
...prev,
...values,
}));
};

if (!isFetching && greaterThanOrEqualTo(impact, severePriceImpactThreshold)) {
runOnUI(updatePriceImpact)({
type: SwapPriceImpactType.severe,
impactDisplay,
});
} else if (!isFetching && greaterThanOrEqualTo(impact, highPriceImpactThreshold)) {
runOnUI(updatePriceImpact)({
type: SwapPriceImpactType.high,
impactDisplay,
});
} else {
runOnUI(updatePriceImpact)({
type: SwapPriceImpactType.none,
impactDisplay,
});
}
},
[currentCurrency, priceImpact]
);

useAnimatedReaction(
() => ({
inputNativeValue: SwapInputController.inputValues.value.inputNativeValue,
outputNativeValue: SwapInputController.inputValues.value.outputNativeValue,
isFetching: isFetching.value,
}),
(current, previous) => {
if (previous?.inputNativeValue !== current.inputNativeValue || previous?.outputNativeValue !== current.outputNativeValue) {
if (!current.inputNativeValue || !current.outputNativeValue) {
priceImpact.value = { impactDisplay: '', type: SwapPriceImpactType.none };
return;
}

runOnJS(calculatePriceImpact)({
inputNativeValue: current.inputNativeValue,
outputNativeValue: current.outputNativeValue,
isFetching: current.isFetching,
});
}
}
);

return priceImpact;
};
10 changes: 9 additions & 1 deletion src/__swaps__/screens/Swap/providers/swap-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useAnimatedSwapStyles } from '@/__swaps__/screens/Swap/hooks/useAnimate
import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles';
import { useSwapNavigation } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation';
import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController';
import { usePriceImpactWarning } from '@/__swaps__/screens/Swap/hooks/usePriceImpactWarning';

interface SwapContextType {
inputProgress: SharedValue<number>;
Expand All @@ -20,6 +21,7 @@ interface SwapContextType {
AnimatedSwapStyles: ReturnType<typeof useAnimatedSwapStyles>;
SwapTextStyles: ReturnType<typeof useSwapTextStyles>;
SwapNavigation: ReturnType<typeof useSwapNavigation>;
PriceImpactWarning: ReturnType<typeof usePriceImpactWarning>;

confirmButtonIcon: Readonly<SharedValue<string>>;
confirmButtonLabel: Readonly<SharedValue<string>>;
Expand Down Expand Up @@ -54,7 +56,12 @@ export const SwapProvider = ({ children }: SwapProviderProps) => {
outputProgress,
});

const AnimatedSwapStyles = useAnimatedSwapStyles({ SwapInputController, inputProgress, outputProgress, isFetching });
const PriceImpactWarning = usePriceImpactWarning({
SwapInputController,
isFetching,
});

const AnimatedSwapStyles = useAnimatedSwapStyles({ SwapInputController, PriceImpactWarning, inputProgress, outputProgress, isFetching });
const SwapTextStyles = useSwapTextStyles({
...SwapInputController,
focusedInput,
Expand Down Expand Up @@ -118,6 +125,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => {
AnimatedSwapStyles,
SwapTextStyles,
SwapNavigation,
PriceImpactWarning,
confirmButtonIcon,
confirmButtonLabel,
confirmButtonIconStyle,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@
"price_impact": {
"losing_prefix": "Losing",
"small_market": "Small Market",
"smal_market_try_smaller_amount": "Small market –– try a smaller amount",
"you_are_losing": "You're losing %{impactDisplay}",
"no_data": "Market Value Unknown",
"label": "Possible loss",
"no_data_subtitle": "If you decide to continue, be sure that you are satisfied with the quoted amount"
Expand Down