From b0acb0030aa43d24604b5df299a294fa5b3859c8 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 27 Jun 2024 14:52:07 +0200 Subject: [PATCH] feat: add increase decrease token percentage --- .storybook/storybook.requires.js | 2 + .../AggregatedPercentage.stories.js | 63 ++ .../AggregatedPercentage.test.tsx | 70 +++ .../AggregatedPercentage.tsx | 93 +++ .../AggregatedPercentage.test.tsx.snap | 45 ++ .../Price/AggregatedPercentage/index.ts | 1 + .../PercentageChange.stories.js | 56 ++ .../PercentageChange.test.tsx | 42 ++ .../PercentageChange/PercentageChange.tsx | 44 ++ .../PercentageChange.test.tsx.snap | 22 + .../Price/PercentageChange/index.ts | 1 + .../__snapshots__/index.test.tsx.snap | 12 +- app/components/UI/AssetElement/index.tsx | 61 +- .../Tokens/__snapshots__/index.test.tsx.snap | 539 ++++++++++++------ app/components/UI/Tokens/index.tsx | 40 +- app/core/Engine.ts | 29 +- storybook/storyLoader.js | 4 + 17 files changed, 914 insertions(+), 210 deletions(-) create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.js create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap create mode 100644 app/component-library/components-temp/Price/AggregatedPercentage/index.ts create mode 100644 app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.js create mode 100644 app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx create mode 100644 app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx create mode 100644 app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap create mode 100644 app/component-library/components-temp/Price/PercentageChange/index.ts diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index b2fdc79d50a4..906409277994 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -116,6 +116,8 @@ const getStories = () => { './app/component-library/components-temp/TagColored/TagColored.stories.tsx': require('../app/component-library/components-temp/TagColored/TagColored.stories.tsx'), './app/components/UI/Name/Name.stories.tsx': require('../app/components/UI/Name/Name.stories.tsx'), "./app/components/UI/SimulationDetails/SimulationDetails.stories.tsx": require("../app/components/UI/SimulationDetails/SimulationDetails.stories.tsx"), + './app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories': require('../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.js'), + './app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories': require('../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.js'), }; }; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.js b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.js new file mode 100644 index 000000000000..d9706d5e2e64 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import AggregatedPercentage from './AggregatedPercentage'; +import { createStore } from 'redux'; +import initialBackgroundState from '../../../../util/test/initial-background-state.json'; + +const mockInitialState = { + wizard: { + step: 1, + }, + engine: { + backgroundState: initialBackgroundState, + }, +}; + +const rootReducer = (state = mockInitialState) => state; +const store = createStore(rootReducer); + +export default { + title: 'Component Library / AggregatedPercentage', + component: AggregatedPercentage, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + ethFiat: 1000, + tokenFiat: 500, + tokenFiat1dAgo: 950, + ethFiat1dAgo: 450, +}; + +export const NegativePercentageChange = Template.bind({}); +NegativePercentageChange.args = { + ethFiat: 900, + tokenFiat: 400, + tokenFiat1dAgo: 950, + ethFiat1dAgo: 1000, +}; + +export const PositivePercentageChange = Template.bind({}); +PositivePercentageChange.args = { + ethFiat: 1100, + tokenFiat: 600, + tokenFiat1dAgo: 500, + ethFiat1dAgo: 1000, +}; + +export const MixedPercentageChange = Template.bind({}); +MixedPercentageChange.args = { + ethFiat: 1050, + tokenFiat: 450, + tokenFiat1dAgo: 500, + ethFiat1dAgo: 1000, +}; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx new file mode 100644 index 000000000000..818ecc2f591f --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import AggregatedPercentage from './AggregatedPercentage'; +import { mockTheme } from '../../../../util/theme'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); +describe('AggregatedPercentage', () => { + beforeEach(() => { + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === selectCurrentCurrency) return 'USD'; + }); + }); + afterEach(() => { + (useSelector as jest.Mock).mockClear(); + }); + it('should render correctly', () => { + const { toJSON } = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders positive percentage change correctly', () => { + const { getByText } = render( + , + ); + + expect(getByText('(+25.00%)')).toBeTruthy(); + expect(getByText('+100 USD')).toBeTruthy(); + + expect(getByText('(+25.00%)').props.style).toMatchObject({ + color: mockTheme.colors.success.default, + textTransform: 'uppercase', + }); + }); + + it('renders negative percentage change correctly', () => { + const { getByText } = render( + , + ); + + expect(getByText('(-30.00%)')).toBeTruthy(); + expect(getByText('-150 USD')).toBeTruthy(); + + expect(getByText('(-30.00%)').props.style).toMatchObject({ + color: mockTheme.colors.error.default, + textTransform: 'uppercase', + }); + }); +}); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx new file mode 100644 index 000000000000..bb037f3824fb --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { fontStyles } from '../../../../styles/common'; +import Text from '../../../../component-library/components/Texts/Text'; +import { View, StyleSheet } from 'react-native'; +import { useTheme } from '../../../../util/theme'; +import { Colors } from '../../../../util/theme/models'; +import { renderFiat } from '../../../../util/number'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; + +const createStyles = (colors: Colors) => + StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + }, + balanceZeroStyle: { + color: colors.text.default, + ...fontStyles.normal, + textTransform: 'uppercase', + }, + balancePositiveStyle: { + color: colors.success.default, + ...fontStyles.normal, + textTransform: 'uppercase', + }, + balanceNegativeStyle: { + color: colors.error.default, + ...fontStyles.normal, + textTransform: 'uppercase', + }, + }); + +const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + +const AggregatedPercentage = ({ + ethFiat, + tokenFiat, + tokenFiat1dAgo, + ethFiat1dAgo, +}: { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; +}) => { + const { colors } = useTheme(); + const styles = createStyles(colors); + const currentCurrency = useSelector(selectCurrentCurrency); + const DECIMALS_TO_SHOW = 2; + + const totalBalance = ethFiat + tokenFiat; + const totalBalance1dAgo = ethFiat1dAgo + tokenFiat1dAgo; + + const amountChange = totalBalance - totalBalance1dAgo; + + const percentageChange = + ((totalBalance - totalBalance1dAgo) / totalBalance1dAgo) * 100 || 0; + + let percentageStyle = styles.balanceZeroStyle; + + if (percentageChange === 0) { + percentageStyle = styles.balanceZeroStyle; + } else if (percentageChange > 0) { + percentageStyle = styles.balancePositiveStyle; + } else { + percentageStyle = styles.balanceNegativeStyle; + } + + const formattedPercentage = isValidAmount(percentageChange) + ? `(${(percentageChange as number) >= 0 ? '+' : ''}${( + percentageChange as number + ).toFixed(2)}%)` + : ''; + + const formattedValuePrice = isValidAmount(amountChange) + ? `${(amountChange as number) >= 0 ? '+' : ''}${renderFiat( + amountChange, + currentCurrency, + DECIMALS_TO_SHOW, + )} ` + : ''; + + return ( + + {formattedValuePrice} + {formattedPercentage} + + ); +}; + +export default AggregatedPercentage; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap new file mode 100644 index 000000000000..0466373e38f6 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggregatedPercentage should render correctly 1`] = ` + + + +20 USD + + + (+11.11%) + + +`; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/index.ts b/app/component-library/components-temp/Price/AggregatedPercentage/index.ts new file mode 100644 index 000000000000..3e7965d02fa3 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/index.ts @@ -0,0 +1 @@ +export { default } from './AggregatedPercentage'; diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.js b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.js new file mode 100644 index 000000000000..59612dcf9ba2 --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PercentageChange from './PercentageChange'; +import { createStore } from 'redux'; +import initialBackgroundState from '../../../../util/test/initial-background-state.json'; + +const mockInitialState = { + wizard: { + step: 1, + }, + engine: { + backgroundState: initialBackgroundState, + }, +}; + +const rootReducer = (state = mockInitialState) => state; +const store = createStore(rootReducer); + +export default { + title: 'Component Library / PercentageChange', + component: PercentageChange, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + value: 0, +}; + +export const PositiveChange = Template.bind({}); +PositiveChange.args = { + value: 5.5, +}; + +export const NegativeChange = Template.bind({}); +NegativeChange.args = { + value: -3.75, +}; + +export const NoChange = Template.bind({}); +NoChange.args = { + value: 0, +}; + +export const InvalidValue = Template.bind({}); +InvalidValue.args = { + value: null, +}; diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx new file mode 100644 index 000000000000..043aaf7e19db --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import PercentageChange from './PercentageChange'; +import { mockTheme } from '../../../../util/theme'; + +describe('PercentageChange', () => { + it('should render correctly', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); + it('displays a positive value correctly', () => { + const { getByText } = render(); + const positiveText = getByText('+5.50%'); + expect(positiveText).toBeTruthy(); + expect(positiveText.props.style).toMatchObject({ + color: mockTheme.colors.success.default, + textTransform: 'uppercase', + }); + }); + + it('displays a negative value correctly', () => { + const { getByText } = render(); + const negativeText = getByText('-3.25%'); + expect(negativeText).toBeTruthy(); + expect(negativeText.props.style).toMatchObject({ + color: mockTheme.colors.error.default, + textTransform: 'uppercase', + }); + }); + + it('handles null value correctly', () => { + const { queryByText } = render(); + expect(queryByText(/\+/)).toBeNull(); + expect(queryByText(/-/)).toBeNull(); + }); + + it('handles undefined value correctly', () => { + const { queryByText } = render(); + expect(queryByText(/\+/)).toBeNull(); + expect(queryByText(/-/)).toBeNull(); + }); +}); diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx new file mode 100644 index 000000000000..1f051a4391d4 --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { fontStyles } from '../../../../styles/common'; +import Text from '../../../../component-library/components/Texts/Text'; +import { View, StyleSheet } from 'react-native'; +import { useTheme } from '../../../../util/theme'; +import { Colors } from '../../../../util/theme/models'; + +const createStyles = (colors: Colors) => + StyleSheet.create({ + balancePositiveStyle: { + color: colors.success.default, + ...fontStyles.normal, + textTransform: 'uppercase', + }, + balanceNegativeStyle: { + color: colors.error.default, + ...fontStyles.normal, + textTransform: 'uppercase', + }, + }); + +const PercentageChange = ({ value }: { value: number | null | undefined }) => { + const { colors } = useTheme(); + const styles = createStyles(colors); + const percentageStyle = + value && value >= 0 + ? styles.balancePositiveStyle + : styles.balanceNegativeStyle; + + const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + + const formattedValue = isValidAmount(value) + ? `${(value as number) >= 0 ? '+' : ''}${(value as number).toFixed(2)}%` + : ''; + + return ( + + {formattedValue} + + ); +}; + +export default PercentageChange; diff --git a/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap b/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap new file mode 100644 index 000000000000..6433cce265cf --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PercentageChange should render correctly 1`] = ` + + + +5.50% + + +`; diff --git a/app/component-library/components-temp/Price/PercentageChange/index.ts b/app/component-library/components-temp/Price/PercentageChange/index.ts new file mode 100644 index 000000000000..6a3f076bb2ae --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/index.ts @@ -0,0 +1 @@ +export { default } from './PercentageChange'; diff --git a/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap b/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap index 5704e5960e90..99fc71bcbfce 100644 --- a/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap @@ -14,5 +14,15 @@ exports[`AssetElement should render correctly 1`] = ` } } testID="asset-DAI" -/> +> + + `; diff --git a/app/components/UI/AssetElement/index.tsx b/app/components/UI/AssetElement/index.tsx index e95c3a3a1f66..fdf638bb1256 100644 --- a/app/components/UI/AssetElement/index.tsx +++ b/app/components/UI/AssetElement/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { TouchableOpacity, StyleSheet, Platform } from 'react-native'; +import { TouchableOpacity, StyleSheet, Platform, View } from 'react-native'; import Text, { TextVariant, } from '../../../component-library/components/Texts/Text'; @@ -12,15 +12,20 @@ import { TOKEN_BALANCE_LOADING, TOKEN_RATE_UNDEFINED, } from '../Tokens/constants'; +import { Colors } from '../../../util/theme/models'; +import { fontStyles } from '../../../styles/common'; +import { useTheme } from '../../../util/theme'; + interface AssetElementProps { children?: React.ReactNode; asset: TokenI; onPress?: (asset: TokenI) => void; onLongPress?: ((asset: TokenI) => void) | null; balance?: string; + mainBalance?: string | null; } -const createStyles = () => +const createStyles = (colors: Colors) => StyleSheet.create({ itemWrapper: { flex: 1, @@ -32,6 +37,7 @@ const createStyles = () => arrow: { flex: 1, alignSelf: 'flex-end', + alignItems: 'flex-end', }, arrowIcon: { marginTop: 16, @@ -39,6 +45,12 @@ const createStyles = () => skeleton: { width: 50, }, + balanceFiat: { + color: colors.text.alternative, + paddingHorizontal: 0, + ...fontStyles.normal, + textTransform: 'uppercase', + }, }); /** @@ -48,10 +60,12 @@ const AssetElement: React.FC = ({ children, balance, asset, + mainBalance = null, onPress, onLongPress, }) => { - const styles = createStyles(); + const { colors } = useTheme(); + const styles = createStyles(colors); const handleOnPress = () => { onPress?.(asset); @@ -70,21 +84,32 @@ const AssetElement: React.FC = ({ > {children} - {balance && ( - - {balance === TOKEN_BALANCE_LOADING ? ( - - ) : ( - balance - )} - - )} + + {balance && ( + + {balance === TOKEN_BALANCE_LOADING ? ( + + ) : ( + balance + )} + + )} + {mainBalance ? ( + + {mainBalance === TOKEN_BALANCE_LOADING ? ( + + ) : ( + mainBalance + )} + + ) : null} + ); }; diff --git a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap index 4aaccc2fc586..eda582ee8983 100644 --- a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap @@ -782,22 +782,22 @@ exports[`Tokens should hide zero balance tokens when setting is on 1`] = ` /> - + - < $0.01 - + /> + - - ETH - + + ETH + + + < $0.01 + + - + - < $0.01 - + /> + - - < 0.00001 BAT - + + < 0.00001 BAT + + + < $0.01 + + - + - < $0.01 - + /> + - - ETH - + + ETH + + + < $0.01 + + - + - < $0.01 - + /> + - - < 0.00001 BAT - + + < 0.00001 BAT + + + < $0.01 + + - + - < $0.01 - + /> + - - ETH - + + ETH + + + < $0.01 + + - + - < $0.01 - + /> + - - < 0.00001 BAT - + + < 0.00001 BAT + + + < $0.01 + + - + - < $0.01 - + /> + - - 0 LINK - + + 0 LINK + + + < $0.01 + + = ({ tokens }) => { const { colors } = useTheme(); @@ -135,6 +137,7 @@ const Tokens: React.FC = ({ tokens }) => { ); const { data: tokenBalances } = useTokenBalancesController(); const tokenExchangeRates = useSelector(selectContractExchangeRates); + const hideZeroBalanceTokens = useSelector( // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -367,6 +370,10 @@ const Tokens: React.FC = ({ tokens }) => { const { balanceFiat, balanceValueFormatted } = handleBalance(asset); + const pricePercentChange1d = itemAddress + ? tokenExchangeRates?.[itemAddress as `0x${string}`]?.pricePercentChange1d + : tokenExchangeRates?.[zeroAddress()]?.pricePercentChange1d; + // render balances according to primary currency let mainBalance, secondaryBalance; mainBalance = TOKEN_BALANCE_LOADING; @@ -434,6 +441,7 @@ const Tokens: React.FC = ({ tokens }) => { onLongPress={asset.isETH ? null : showRemoveMenu} asset={asset} balance={secondaryBalance} + mainBalance={mainBalance} > = ({ tokens }) => { {/** Add button link to Portfolio Stake if token is mainnet ETH */} {asset.isETH && isMainnet && renderStakeButton(asset)} - - - {mainBalance === TOKEN_BALANCE_LOADING ? ( - - ) : ( - mainBalance - )} - + {renderScamWarningIcon(asset)} @@ -622,12 +623,21 @@ const Tokens: React.FC = ({ tokens }) => { return ( - - {fiatBalance} - + + + {fiatBalance} + + + +