-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add increase decrease token percentage (#10144)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This pull request introduces a new feature to the MetaMask mobile that enhances user experience by displaying the percentage increase or decrease for each token directly within the UI. This update aims to provide users with immediate visual feedback on the performance of their tokens, helping them make more informed decisions based on recent market trends. core PR: MetaMask/core#4206 figma: https://www.figma.com/design/aMYisczaJyEsYl1TYdcPUL/Wallet-Assets?node-id=1620-23897&t=EJSzfKoFvTJ5LuK0-0 ## **Related issues** Fixes: #9635 ## **Manual testing steps** 1. Go to the wallet view 2. You should see the percentage of increase/decrease ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <img width="487" alt="Screenshot 2024-05-15 at 16 54 25" src="https://github.com/MetaMask/metamask-mobile/assets/26223211/5b165737-4150-4ca7-83a3-3cb667b6f4a6"> ### **After** ![Screenshot 2024-05-15 at 16 39 31](https://github.com/MetaMask/metamask-mobile/assets/26223211/3792f1d2-6300-4cad-ad5a-6c49e1697773) ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
- Loading branch information
Showing
18 changed files
with
1,099 additions
and
292 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
...onent-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) => ( | ||
<Provider store={store}> | ||
<Story /> | ||
</Provider> | ||
), | ||
], | ||
}; | ||
|
||
const Template = (args) => <AggregatedPercentage {...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, | ||
}; |
20 changes: 20 additions & 0 deletions
20
...mponent-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Third party dependencies. | ||
import { StyleSheet } from 'react-native'; | ||
|
||
/** | ||
* Style sheet function for AggregatedPercentage component. | ||
* | ||
* @param params Style sheet params. | ||
* @param params.theme App theme from ThemeContext. | ||
* @param params.vars Inputs that the style sheet depends on. | ||
* @returns StyleSheet object. | ||
*/ | ||
const styleSheet = () => | ||
StyleSheet.create({ | ||
wrapper: { | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
}, | ||
}); | ||
|
||
export default styleSheet; |
68 changes: 68 additions & 0 deletions
68
...omponent-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
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( | ||
<AggregatedPercentage | ||
ethFiat={100} | ||
tokenFiat={100} | ||
tokenFiat1dAgo={90} | ||
ethFiat1dAgo={90} | ||
/>, | ||
); | ||
expect(toJSON()).toMatchSnapshot(); | ||
}); | ||
|
||
it('renders positive percentage change correctly', () => { | ||
const { getByText } = render( | ||
<AggregatedPercentage | ||
ethFiat={200} | ||
tokenFiat={300} | ||
tokenFiat1dAgo={250} | ||
ethFiat1dAgo={150} | ||
/>, | ||
); | ||
|
||
expect(getByText('(+25.00%)')).toBeTruthy(); | ||
expect(getByText('+100 USD')).toBeTruthy(); | ||
|
||
expect(getByText('(+25.00%)').props.style).toMatchObject({ | ||
color: mockTheme.colors.success.default, | ||
}); | ||
}); | ||
|
||
it('renders negative percentage change correctly', () => { | ||
const { getByText } = render( | ||
<AggregatedPercentage | ||
ethFiat={150} | ||
tokenFiat={200} | ||
tokenFiat1dAgo={300} | ||
ethFiat1dAgo={200} | ||
/>, | ||
); | ||
|
||
expect(getByText('(-30.00%)')).toBeTruthy(); | ||
expect(getByText('-150 USD')).toBeTruthy(); | ||
|
||
expect(getByText('(-30.00%)').props.style).toMatchObject({ | ||
color: mockTheme.colors.error.default, | ||
}); | ||
}); | ||
}); |
76 changes: 76 additions & 0 deletions
76
app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import React from 'react'; | ||
import Text, { | ||
TextColor, | ||
TextVariant, | ||
} from '../../../../component-library/components/Texts/Text'; | ||
import { View } from 'react-native'; | ||
import { renderFiat } from '../../../../util/number'; | ||
import { useSelector } from 'react-redux'; | ||
import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; | ||
import styleSheet from './AggregatedPercentage.styles'; | ||
import { useStyles } from '../../../hooks'; | ||
|
||
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 { styles } = useStyles(styleSheet, {}); | ||
|
||
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 percentageTextColor = TextColor.Default; | ||
|
||
if (percentageChange === 0) { | ||
percentageTextColor = TextColor.Default; | ||
} else if (percentageChange > 0) { | ||
percentageTextColor = TextColor.Success; | ||
} else { | ||
percentageTextColor = TextColor.Error; | ||
} | ||
|
||
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 ( | ||
<View style={styles.wrapper}> | ||
<Text color={percentageTextColor} variant={TextVariant.BodyMDMedium}> | ||
{formattedValuePrice} | ||
</Text> | ||
<Text color={percentageTextColor} variant={TextVariant.BodyMDMedium}> | ||
{formattedPercentage} | ||
</Text> | ||
</View> | ||
); | ||
}; | ||
|
||
export default AggregatedPercentage; |
43 changes: 43 additions & 0 deletions
43
...mponents-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`AggregatedPercentage should render correctly 1`] = ` | ||
<View | ||
style={ | ||
{ | ||
"alignItems": "center", | ||
"flexDirection": "row", | ||
} | ||
} | ||
> | ||
<Text | ||
accessibilityRole="text" | ||
style={ | ||
{ | ||
"color": "#1c8234", | ||
"fontFamily": "Euclid Circular B", | ||
"fontSize": 14, | ||
"fontWeight": "500", | ||
"letterSpacing": 0, | ||
"lineHeight": 22, | ||
} | ||
} | ||
> | ||
+20 USD | ||
</Text> | ||
<Text | ||
accessibilityRole="text" | ||
style={ | ||
{ | ||
"color": "#1c8234", | ||
"fontFamily": "Euclid Circular B", | ||
"fontSize": 14, | ||
"fontWeight": "500", | ||
"letterSpacing": 0, | ||
"lineHeight": 22, | ||
} | ||
} | ||
> | ||
(+11.11%) | ||
</Text> | ||
</View> | ||
`; |
1 change: 1 addition & 0 deletions
1
app/component-library/components-temp/Price/AggregatedPercentage/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './AggregatedPercentage'; |
56 changes: 56 additions & 0 deletions
56
app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) => ( | ||
<Provider store={store}> | ||
<Story /> | ||
</Provider> | ||
), | ||
], | ||
}; | ||
|
||
const Template = (args) => <PercentageChange {...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, | ||
}; |
40 changes: 40 additions & 0 deletions
40
app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
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(<PercentageChange value={5.5} />); | ||
expect(toJSON()).toMatchSnapshot(); | ||
}); | ||
it('displays a positive value correctly', () => { | ||
const { getByText } = render(<PercentageChange value={5.5} />); | ||
const positiveText = getByText('+5.50%'); | ||
expect(positiveText).toBeTruthy(); | ||
expect(positiveText.props.style).toMatchObject({ | ||
color: mockTheme.colors.success.default, | ||
}); | ||
}); | ||
|
||
it('displays a negative value correctly', () => { | ||
const { getByText } = render(<PercentageChange value={-3.25} />); | ||
const negativeText = getByText('-3.25%'); | ||
expect(negativeText).toBeTruthy(); | ||
expect(negativeText.props.style).toMatchObject({ | ||
color: mockTheme.colors.error.default, | ||
}); | ||
}); | ||
|
||
it('handles null value correctly', () => { | ||
const { queryByText } = render(<PercentageChange value={null} />); | ||
expect(queryByText(/\+/)).toBeNull(); | ||
expect(queryByText(/-/)).toBeNull(); | ||
}); | ||
|
||
it('handles undefined value correctly', () => { | ||
const { queryByText } = render(<PercentageChange value={undefined} />); | ||
expect(queryByText(/\+/)).toBeNull(); | ||
expect(queryByText(/-/)).toBeNull(); | ||
}); | ||
}); |
Oops, something went wrong.