Skip to content

Commit

Permalink
feat: add increase decrease token percentage (#10144)
Browse files Browse the repository at this point in the history
<!--
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
salimtb authored Jul 3, 2024
1 parent 62ed9fe commit 0f40b8d
Show file tree
Hide file tree
Showing 18 changed files with 1,099 additions and 292 deletions.
2 changes: 2 additions & 0 deletions .storybook/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ const getStories = () => {
'./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/CellSelectWithMenu/CellSelectWithMenu.stories.tsx": require("../app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx"),
'./app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories': require('../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx'),
'./app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories': require('../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx'),
};
};

Expand Down
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,
};
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;
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,
});
});
});
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;
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>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AggregatedPercentage';
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,
};
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();
});
});
Loading

0 comments on commit 0f40b8d

Please sign in to comment.