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

[WIP]: Swaps v2 quote fetching #5601

Merged
merged 21 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"@notifee/react-native": "5.6.0",
"@rainbow-me/provider": "0.0.11",
"@rainbow-me/react-native-animated-number": "0.0.2",
"@rainbow-me/swaps": "0.15.0",
"@rainbow-me/swaps": "0.16.0",
"@react-native-async-storage/async-storage": "1.18.2",
"@react-native-camera-roll/camera-roll": "5.7.1",
"@react-native-clipboard/clipboard": "1.13.2",
Expand Down
151 changes: 133 additions & 18 deletions src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,148 @@
import React from 'react';
import Animated, { useDerivedValue } from 'react-native-reanimated';
import React, { useState } from 'react';
import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
import { AnimatedText, Box, Inline, TextIcon, useColorMode, useForegroundColor } from '@/design-system';
import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants';
import { opacity } from '@/__swaps__/utils/swaps';
import { opacity, priceForAsset, valueBasedDecimalFormatter } from '@/__swaps__/utils/swaps';
import { ButtonPressAnimation } from '@/components/animations';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';

export const ExchangeRateBubble = () => {
const { isDarkMode } = useColorMode();
const { AnimatedSwapStyles, SwapInputController } = useSwapContext();

const [exchangeRateIndex, setExchangeRateIndex] = useState<number>(0);

const assetToSellPrice = useSharedValue(0);
const assetToBuyPrice = useSharedValue(0);
const assetToSellSymbol = useSharedValue('');
const assetToBuySymbol = useSharedValue('');
const fromAssetText = useSharedValue('');
const toAssetText = useSharedValue('');

const fillTertiary = useForegroundColor('fillTertiary');

const assetToSellLabel = useDerivedValue(() => {
if (!SwapInputController?.assetToSell.value) return '';
return `1 ${SwapInputController?.assetToSell.value?.symbol}`;
});
useAnimatedReaction(
() => ({
assetToSell: SwapInputController.assetToSell.value,
assetToBuy: SwapInputController.assetToBuy.value,
assetToSellPrice: SwapInputController.assetToSellPrice.value,
assetToBuyPrice: SwapInputController.assetToBuyPrice.value,
exchangeRateIndex,
}),
(current, previous) => {
if (current.assetToSell && (!previous?.assetToSell || current.assetToSell !== previous.assetToSell)) {
assetToSellSymbol.value = current.assetToSell.symbol;

const assetToBuyLabel = useDerivedValue(() => {
if (!SwapInputController.assetToBuy.value) return '';
return `1,624.04 ${SwapInputController.assetToBuy.value?.symbol}`;
});
// try to set price immediately
const price = priceForAsset({
asset: current.assetToSell,
assetType: 'assetToSell',
assetToSellPrice: SwapInputController.assetToSellPrice,
assetToBuyPrice: SwapInputController.assetToBuyPrice,
});

if (price) {
assetToSellPrice.value = price;
}
}

if (current.assetToBuy && (!previous?.assetToBuy || current.assetToBuy !== previous.assetToBuy)) {
assetToBuySymbol.value = current.assetToBuy.symbol;

// try to set price immediately
const price = priceForAsset({
asset: current.assetToBuy,
assetType: 'assetToBuy',
assetToSellPrice: SwapInputController.assetToSellPrice,
assetToBuyPrice: SwapInputController.assetToBuyPrice,
});

// TODO: Do proper exchange rate calculation once we receive the quote
if (price) {
assetToBuyPrice.value = price;
}
}

// TODO: This doesn't work when assets change, figure out why...
if (!assetToSellLabel.value || !assetToBuyLabel.value) return null;
if (current.assetToSell && current.assetToBuy) {
runOnJS(SwapInputController.fetchAssetPrices)({
assetToSell: current.assetToSell,
assetToBuy: current.assetToBuy,
});
}

if (current.assetToSellPrice && (!previous?.assetToSellPrice || current.assetToSellPrice !== previous.assetToSellPrice)) {
assetToSellPrice.value = current.assetToSellPrice;
}

if (current.assetToBuyPrice && (!previous?.assetToBuyPrice || current.assetToBuyPrice !== previous.assetToBuyPrice)) {
assetToBuyPrice.value = current.assetToBuyPrice;
}

if (assetToSellPrice.value && assetToBuyPrice.value) {
switch (exchangeRateIndex) {
// 1 assetToSell => x assetToBuy
case 0: {
const formattedRate = valueBasedDecimalFormatter(
assetToSellPrice.value / assetToBuyPrice.value,
assetToBuyPrice.value,
'up',
-1,
current.assetToBuy?.type === 'stablecoin' ?? false,
false
);

fromAssetText.value = `1 ${assetToSellSymbol.value}`;
toAssetText.value = `${formattedRate} ${assetToBuySymbol.value}`;
break;
}
// 1 assetToBuy => x assetToSell
case 1: {
const formattedRate = valueBasedDecimalFormatter(
assetToBuyPrice.value / assetToSellPrice.value,
assetToSellPrice.value,
'up',
-1,
current.assetToSell?.type === 'stablecoin' ?? false,
false
);
fromAssetText.value = `1 ${assetToBuySymbol.value}`;
toAssetText.value = `${formattedRate} ${assetToSellSymbol.value}`;
break;
}
// assetToSell => native currency
case 2: {
fromAssetText.value = `1 ${assetToSellSymbol.value}`;
toAssetText.value = `$${assetToSellPrice.value.toLocaleString('en-US', {
useGrouping: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

we shouldnt be using toLocaleString since it doenst play with with the rest of the supported currencies, we have a JS util might just need to write it as a worklet

Copy link
Contributor Author

Choose a reason for hiding this comment

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

minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
break;
}
// assetToBuy => native currency
case 3: {
fromAssetText.value = `1 ${assetToBuySymbol.value}`;
toAssetText.value = `$${assetToBuyPrice.value.toLocaleString('en-US', {
useGrouping: true,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
break;
}
}
}
}
);

const WrapperStyles = useAnimatedStyle(() => {
return {
borderColor: isDarkMode ? SEPARATOR_COLOR : LIGHT_SEPARATOR_COLOR,
borderWidth: THICK_BORDER_WIDTH,
opacity: fromAssetText.value && toAssetText.value ? 1 : 0,
};
});

return (
<ButtonPressAnimation scaleTo={0.925} style={{ marginTop: 4 }}>
<ButtonPressAnimation onPress={() => setExchangeRateIndex((exchangeRateIndex + 1) % 4)} scaleTo={0.925} style={{ marginTop: 4 }}>
<Box
walmat marked this conversation as resolved.
Show resolved Hide resolved
as={Animated.View}
alignItems="center"
Expand All @@ -38,12 +152,13 @@ export const ExchangeRateBubble = () => {
style={[AnimatedSwapStyles.hideWhenInputsExpanded, { alignSelf: 'center' }]}
>
<Box
as={Animated.View}
alignItems="center"
borderRadius={15}
height={{ custom: 30 }}
justifyContent="center"
paddingHorizontal="10px"
style={{ borderColor: isDarkMode ? SEPARATOR_COLOR : LIGHT_SEPARATOR_COLOR, borderWidth: THICK_BORDER_WIDTH }}
style={WrapperStyles}
>
<Inline alignHorizontal="center" alignVertical="center" space="6px" wrap={false}>
<AnimatedText
Expand All @@ -52,7 +167,7 @@ export const ExchangeRateBubble = () => {
size="13pt"
style={{ opacity: isDarkMode ? 0.6 : 0.75 }}
weight="heavy"
text={assetToSellLabel}
text={fromAssetText}
/>
<Box
borderRadius={10}
Expand All @@ -71,7 +186,7 @@ export const ExchangeRateBubble = () => {
size="13pt"
style={{ opacity: isDarkMode ? 0.6 : 0.75 }}
weight="heavy"
text={assetToBuyLabel}
text={toAssetText}
/>
</Inline>
</Box>
Expand Down
54 changes: 37 additions & 17 deletions src/__swaps__/screens/Swap/components/FlipButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable no-nested-ternary */
import c from 'chroma-js';
import React, { useCallback } from 'react';
import Animated, { runOnUI } from 'react-native-reanimated';
import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
import SwapSpinner from '@/__swaps__/assets/swapSpinner.png';
import { ButtonPressAnimation } from '@/components/animations';
import { AnimatedSpinner } from '@/__swaps__/components/animations/AnimatedSpinner';
import { Bleed, Box, IconContainer, Text, globalColors, useColorMode } from '@/design-system';
import { colors } from '@/styles';
import { SEPARATOR_COLOR } from '@/__swaps__/screens/Swap/constants';
import { opacity } from '@/__swaps__/utils/swaps';
import { getMixedColor, opacity } from '@/__swaps__/utils/swaps';
import { IS_ANDROID, IS_IOS } from '@/env';
import { AnimatedBlurView } from '@/__swaps__/screens/Swap/components/AnimatedBlurView';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
Expand All @@ -19,9 +19,41 @@ export const FlipButton = () => {

const { isFetching, AnimatedSwapStyles, SwapInputController } = useSwapContext();

const shadowColor = useSharedValue(
isDarkMode ? globalColors.grey100 : c.mix(SwapInputController.bottomColor.value, colors.dark, 0.84).hex()
);

const handleSwapAssets = useCallback(() => {
runOnUI(SwapInputController.onSwapAssets)();
}, [SwapInputController.onSwapAssets]);
SwapInputController.onSwapAssets();
}, [SwapInputController]);

const getBottomColor = ({ bottomColor }: { bottomColor: string }) => {
shadowColor.value = getMixedColor(bottomColor, colors.dark, 0.84);
};

useAnimatedReaction(
() => ({
bottomColor: SwapInputController.bottomColor.value,
}),
(current, previous) => {
if (previous && current !== previous && current !== undefined) {
runOnJS(getBottomColor)(current);
}
}
);

const flipButtonInnerStyles = useAnimatedStyle(() => {
return {
shadowColor: shadowColor.value,
shadowOffset: {
width: 0,
height: isDarkMode ? 4 : 4,
},
elevation: 8,
shadowOpacity: isDarkMode ? 0.3 : 0.1,
shadowRadius: isDarkMode ? 6 : 8,
};
});

return (
<Box
Expand All @@ -30,19 +62,7 @@ export const FlipButton = () => {
justifyContent="center"
style={[AnimatedSwapStyles.flipButtonStyle, AnimatedSwapStyles.focusedSearchStyle, { height: 12, width: 28, zIndex: 10 }]}
>
<Box
as={Animated.View}
style={{
shadowColor: isDarkMode ? globalColors.grey100 : c.mix(SwapInputController.bottomColor.value, colors.dark, 0.84).hex(),
shadowOffset: {
width: 0,
height: isDarkMode ? 4 : 4,
},
elevation: 8,
shadowOpacity: isDarkMode ? 0.3 : 0.1,
shadowRadius: isDarkMode ? 6 : 8,
}}
>
<Box as={Animated.View} style={flipButtonInnerStyles}>
<ButtonPressAnimation onPress={handleSwapAssets} scaleTo={0.8} style={{ paddingHorizontal: 20, paddingVertical: 8 }}>
{/* TODO: Temp fix - rewrite to actually avoid type errors */}
{/* @ts-expect-error The conditional as={} is causing type errors */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const TokenToBuyList = () => {
);

return (
<Stack space="32px">
<Stack space="24px">
<Box paddingHorizontal="20px">
<Inline alignHorizontal="justify" alignVertical="center">
<Inline alignVertical="center" space="6px">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { TextStyle } from 'react-native';
import Animated, { runOnUI } from 'react-native-reanimated';
import Animated, { useDerivedValue } from 'react-native-reanimated';
import { FlashList } from '@shopify/flash-list';

import * as i18n from '@/languages';
import { CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow';
import { useSwapAssetStore } from '@/__swaps__/screens/Swap/state/assets';
import { SearchAsset } from '@/__swaps__/types/search';
import { Box, Inline, Inset, Stack, Text } from '@/design-system';
import { AnimatedText, Box, Inline, Inset, Stack, Text } from '@/design-system';
import { AssetToBuySection, AssetToBuySectionId } from '@/__swaps__/screens/Swap/hooks/useSearchCurrencyLists';
import { ChainId } from '@/__swaps__/types/chains';
import { TextColor } from '@/design-system/color/palettes';
Expand Down Expand Up @@ -67,7 +66,6 @@ const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList<Se

export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) => {
const { SwapInputController } = useSwapContext();
const { outputChainId } = useSwapAssetStore();
const userAssets = useAssetsToSell();

const handleSelectToken = useCallback(
Expand All @@ -79,25 +77,28 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) =
userAsset,
});

runOnUI(SwapInputController.onSetAssetToBuy)(parsedAsset);
SwapInputController.onSetAssetToBuy(parsedAsset);
},
[SwapInputController, userAssets]
);

const { symbol, title } = sectionProps[section.id];

const color = useMemo(() => {
const symbolValue = useDerivedValue(() => symbol);

const color = useDerivedValue(() => {
if (section.id !== 'bridge') {
return sectionProps[section.id].color as TextColor;
}
return bridgeSectionsColorsByChain[outputChainId || ChainId.mainnet] as TextColor;
}, [section.id, outputChainId]);

return bridgeSectionsColorsByChain[SwapInputController.outputChainId.value || ChainId.mainnet] as TextColor;
});

if (!section.data.length) return null;

return (
<Box key={section.id} testID={`${section.id}-token-to-buy-section`}>
<Stack space="20px">
<Stack space="8px">
{section.id === 'other_networks' ? (
<Box borderRadius={12} height={{ custom: 52 }}>
<Inset horizontal="20px" vertical="8px">
Expand All @@ -112,9 +113,12 @@ export const TokenToBuySection = ({ section }: { section: AssetToBuySection }) =
) : null}
<Box paddingHorizontal={'20px'}>
<Inline space="6px" alignVertical="center">
<Text size="14px / 19px (Deprecated)" weight="heavy" color={section.id === 'bridge' ? color : { custom: color }}>
{symbol}
</Text>
<AnimatedText
size="14px / 19px (Deprecated)"
weight="heavy"
color={section.id === 'bridge' ? color.value : { custom: color.value }}
text={symbolValue}
/>
<Text size="14px / 19px (Deprecated)" weight="heavy" color="label">
{title}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const TokenToSellList = () => {
</Inline>
</Box>
<AnimatedFlashListComponent
data={userAssets}
data={userAssets.slice(0, 20)}
ListEmptyComponent={<ListEmpty />}
keyExtractor={item => item.uniqueId}
renderItem={({ item }) => (
Expand Down