Skip to content

Commit

Permalink
fix: simulation Fiat precision and Fiat flickers different value befo…
Browse files Browse the repository at this point in the history
…re decimals are applied (#13371)

<!--
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**

- Fix fiat precision. We should show expected digits instead of 0s. 
  - Replaces areas where value is coerced to number
- Fix related FiatDisplay test
- invalid test where we called toBeDefined instead of toBeTruthy from
queryByText
  - removed useFiatFormatter mock
  - fix invalid showFiatInTestnets mock
- fix hideFiatForTestnet test related mocks since in
useHideFiatForTestnet
```TEST_NETWORK_IDS.includes(chainId) && !showFiatInTestnets;``` was
returning true in all cases as chainId was undefined.
- Update FiatDisplay value to replace $100.00 → $100 and $100.2 →
$100.20. useFiatFormatter formatted as expected.
- Fixes fiat flickering issue while data is still being fetched - adds
placeholder element while token details are pending

## **Related issues**

Fixes: #13387

## **Manual testing steps**

1. Go to https://develop.d3bkcslj57l47p.amplifyapp.com/
2. Trigger Permit Batch

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<img width="320"
src="https://github.com/user-attachments/assets/30dd5d66-91a3-4fc1-8a6a-c5ede4134c8c">

### **After example**

<img width="320"
src="https://github.com/user-attachments/assets/c5ad4c15-bed8-4428-a7f0-bbcf37f5c9c0">

### **Real after with hidden value if "Unlimited" is shown w/ some token
icons still loading **

<img width="320"
src="https://github.com/user-attachments/assets/b8452419-254a-46d8-93af-fd083535e949">

### **Before flickering issue **


https://github.com/user-attachments/assets/52178518-04ed-49fd-8568-d40d1b6c0637

### **After** 


https://github.com/user-attachments/assets/2ef9fc1a-bd0f-457b-bf20-9e5250b1df97


## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] 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
digiwand authored Feb 13, 2025
1 parent 9798e95 commit 9911443
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 147 deletions.
148 changes: 84 additions & 64 deletions app/components/UI/SimulationDetails/FiatDisplay/FiatDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,67 @@
import React from 'react';
import BigNumber from 'bignumber.js';
import { merge } from 'lodash';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../util/test/initial-root-state';

import { IndividualFiatDisplay, TotalFiatDisplay } from './FiatDisplay';
import { FIAT_UNAVAILABLE } from '../types';
import useFiatFormatter from './useFiatFormatter';
import { mockNetworkState } from '../../../../util/test/network';
import { CHAIN_IDS } from '@metamask/transaction-controller';

jest.mock('./useFiatFormatter');

const mockInitialState = {
engine: {
backgroundState,
},
};
import { FIAT_UNAVAILABLE, FiatAmount } from '../types';

const mockStateWithTestnet = merge({}, mockInitialState, {
engine: {
backgroundState: {
NetworkController: {
...mockNetworkState({
chainId: CHAIN_IDS.SEPOLIA,
id: 'sepolia',
nickname: 'Sepolia',
ticker: 'ETH',
}),
},
const mockStateWithTestnet = merge(
{
engine: {
backgroundState,
},
},
});

const mockStateWithShowingFiatOnTestnets = merge({}, mockStateWithTestnet, {
engine: {
backgroundState: {
PreferencesController: {
showFiatInTestnets: true,
{
engine: {
backgroundState: {
CurrencyRateController: {
currentCurrency: 'USD',
},
NetworkController: {
...mockNetworkState({
chainId: CHAIN_IDS.SEPOLIA,
id: 'sepolia',
nickname: 'Sepolia',
ticker: 'ETH',
}),
},
},
},
settings: {
showFiatOnTestnets: true,
},
},
});
);

const mockStateWithHidingFiatOnTestnets = merge({}, mockStateWithTestnet, {
engine: {
backgroundState: {
PreferencesController: {
showFiatInTestnets: false,
},
},
const mockStateWithHideFiatOnTestnets = merge({}, mockStateWithTestnet, {
settings: {
showFiatOnTestnets: false,
},
});

describe('FiatDisplay', () => {
const mockUseFiatFormatter = jest.mocked(useFiatFormatter);

beforeEach(() => {
jest.resetAllMocks();
mockUseFiatFormatter.mockReturnValue((value: number) => `$${value}`);
});

describe('IndividualFiatDisplay', () => {
it.each([
[FIAT_UNAVAILABLE, 'Not Available'],
[100, '$100'],
[-100, '$100'],
])('when fiatAmount is %s it renders %s', (fiatAmount, expected) => {
const { queryByText } = renderWithProvider(
<IndividualFiatDisplay fiatAmount={fiatAmount} />,
{ state: mockStateWithShowingFiatOnTestnets },
[new BigNumber(100), '$100'],
[new BigNumber(-100), '$100'],
[new BigNumber(-100.5), '$100.50'],
[new BigNumber('987543219876543219876.54321'), '$987,543,219,87...'],
])('when fiatAmount is %s it renders %s', async (fiatAmount, expected) => {
const { findByText } = renderWithProvider(
<IndividualFiatDisplay fiatAmount={fiatAmount as BigNumber} />,
{ state: mockStateWithTestnet },
);
expect(queryByText(expected)).toBeDefined();
expect(await findByText(expected)).toBeTruthy();
});

it('does not render anything if hideFiatForTestnet is true', () => {
const { queryByText } = renderWithProvider(
<IndividualFiatDisplay fiatAmount={100} />,
{ state: mockStateWithHidingFiatOnTestnets },
{ state: mockStateWithHideFiatOnTestnets },
);
expect(queryByText('100')).toBe(null);
});
Expand All @@ -86,20 +71,55 @@ describe('FiatDisplay', () => {
it.each([
[[FIAT_UNAVAILABLE, FIAT_UNAVAILABLE], 'Not Available'],
[[], 'Not Available'],
[[100, 200, FIAT_UNAVAILABLE, 300], 'Total = $600'],
[[-100, -200, FIAT_UNAVAILABLE, -300], 'Total = $600'],
])('when fiatAmounts is %s it renders %s', (fiatAmounts, expected) => {
const { queryByText } = renderWithProvider(
<TotalFiatDisplay fiatAmounts={fiatAmounts} />,
{ state: mockStateWithShowingFiatOnTestnets },
);
expect(queryByText(expected)).toBeDefined();
});
[
[
new BigNumber(100),
new BigNumber(200),
FIAT_UNAVAILABLE,
new BigNumber(300),
],
'Total = $600',
],
[
[
new BigNumber(-100),
new BigNumber(-200),
FIAT_UNAVAILABLE,
new BigNumber(-300.2),
],
'Total = $600.20',
],
[
[
new BigNumber(new BigNumber('987543219876543219876.54321')),
new BigNumber(-200),
FIAT_UNAVAILABLE,
new BigNumber(-300.2),
],
'Total = $987,543,219,876,543,219,376.34',
],
])(
'when fiatAmounts is %s it renders %s',
async (fiatAmounts, expected) => {
const { findByText } = renderWithProvider(
<TotalFiatDisplay fiatAmounts={fiatAmounts as FiatAmount[]} />,
{ state: mockStateWithTestnet },
);

expect(await findByText(expected)).toBeTruthy();
},
);

it('does not render anything if hideFiatForTestnet is true', () => {
const mockFiatAmounts = [
new BigNumber(100),
new BigNumber(200),
new BigNumber(300),
] as unknown as FiatAmount[];

const { queryByText } = renderWithProvider(
<TotalFiatDisplay fiatAmounts={[100, 200, 300]} />,
{ state: mockStateWithHidingFiatOnTestnets },
<TotalFiatDisplay fiatAmounts={mockFiatAmounts} />,
{ state: mockStateWithHideFiatOnTestnets },
);
expect(queryByText('600')).toBe(null);
});
Expand Down
29 changes: 21 additions & 8 deletions app/components/UI/SimulationDetails/FiatDisplay/FiatDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { StyleSheet, ViewProps } from 'react-native';
import { BigNumber } from 'bignumber.js';
import { useStyles } from '../../../hooks/useStyles';
import Text, {
TextColor,
Expand All @@ -10,6 +11,7 @@ import { strings } from '../../../../../locales/i18n';
import useFiatFormatter from './useFiatFormatter';
import { FIAT_UNAVAILABLE, FiatAmount } from '../types';
import useHideFiatForTestnet from '../../../hooks/useHideFiatForTestnet';
import { shortenString } from '../../../../util/notifications';

const styleSheet = () =>
StyleSheet.create({
Expand All @@ -33,10 +35,10 @@ const FiatNotAvailableDisplay: React.FC = () => {
);
};

export function calculateTotalFiat(fiatAmounts: FiatAmount[]): number {
export function calculateTotalFiat(fiatAmounts: FiatAmount[]): BigNumber {
return fiatAmounts.reduce(
(total: number, fiat) => total + (fiat === FIAT_UNAVAILABLE ? 0 : fiat),
0,
(total: BigNumber, fiat) => total.plus(fiat === FIAT_UNAVAILABLE ? new BigNumber(0) : new BigNumber(fiat)),
new BigNumber(0)
);
}

Expand All @@ -48,11 +50,13 @@ export function calculateTotalFiat(fiatAmounts: FiatAmount[]): number {
*/

interface IndividualFiatDisplayProps extends ViewProps {
fiatAmount: FiatAmount;
fiatAmount: BigNumber | FiatAmount;
shorten?: boolean;
}

export const IndividualFiatDisplay: React.FC<IndividualFiatDisplayProps> = ({
fiatAmount,
shorten = true,
}) => {
const hideFiatForTestnet = useHideFiatForTestnet();
const { styles } = useStyles(styleSheet, {});
Expand All @@ -65,11 +69,20 @@ export const IndividualFiatDisplay: React.FC<IndividualFiatDisplayProps> = ({
if (fiatAmount === FIAT_UNAVAILABLE) {
return <FiatNotAvailableDisplay />;
}
const absFiat = Math.abs(fiatAmount);
const absFiat = new BigNumber(fiatAmount).abs();

const absFiatFormatted = shorten
? shortenString(fiatFormatter(absFiat), {
truncatedCharLimit: 15,
truncatedStartChars: 15,
truncatedEndChars: 0,
skipCharacterInEnd: true,
})
: fiatFormatter(absFiat);

return (
<Text {...sharedTextProps} style={styles.base}>
{fiatFormatter(absFiat)}
{absFiatFormatted}
</Text>
);
};
Expand All @@ -92,12 +105,12 @@ export const TotalFiatDisplay: React.FC<{
return null;
}

return totalFiat === 0 ? (
return totalFiat.eq(0) ? (
<FiatNotAvailableDisplay />
) : (
<Text {...sharedTextProps} style={styles.base}>
{strings('simulation_details.total_fiat', {
currency: fiatFormatter(Math.abs(totalFiat)),
currency: fiatFormatter(totalFiat.abs()),
})}
</Text>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react-hooks';
import I18n from '../../../../../locales/i18n';
import { selectCurrentCurrency } from '../../../../selectors/currencyRateController';
import useFiatFormatter from './useFiatFormatter';
import { BigNumber } from 'bignumber.js';

jest.mock('react-redux', () => ({
useSelector: jest.fn((selector) => selector()),
Expand Down Expand Up @@ -33,9 +34,10 @@ describe('useFiatFormatter', () => {
const { result } = renderHook(() => useFiatFormatter());
const formatFiat = result.current;

expect(formatFiat(1000)).toBe('$1,000.00');
expect(formatFiat(500.5)).toBe('$500.50');
expect(formatFiat(0)).toBe('$0.00');
expect(formatFiat(new BigNumber('987543219876543219876.54321'))).toBe('$987,543,219,876,543,219,876.54');
expect(formatFiat(new BigNumber(1000))).toBe('$1,000');
expect(formatFiat(new BigNumber(500.5))).toBe('$500.50');
expect(formatFiat(new BigNumber(0))).toBe('$0');
});

it('should use the current locale and currency from the mocked functions', () => {
Expand All @@ -56,8 +58,9 @@ describe('useFiatFormatter', () => {
const formatFiat = result.current;

// Testing the fallback formatting for an unknown currency
expect(formatFiat(1000)).toBe('1000 storj');
expect(formatFiat(500.5)).toBe('500.5 storj');
expect(formatFiat(0)).toBe('0 storj');
expect(formatFiat(new BigNumber('98754321987654321987654321'))).toBe('98754321987654321987654321 storj');
expect(formatFiat(new BigNumber(1000))).toBe('1000 storj');
expect(formatFiat(new BigNumber(500.5))).toBe('500.5 storj');
expect(formatFiat(new BigNumber(0))).toBe('0 storj');
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useSelector } from 'react-redux';
import { selectCurrentCurrency } from '../../../../selectors/currencyRateController';
import I18n from '../../../../../locales/i18n';
import { BigNumber } from 'bignumber.js';

type FiatFormatter = (fiatAmount: number) => string;
type FiatFormatter = (fiatAmount: BigNumber) => string;

/**
* Returns a function that formats a fiat amount as a localized string.
Expand All @@ -11,23 +12,29 @@ type FiatFormatter = (fiatAmount: number) => string;
*
* ```
* const formatFiat = useFiatFormatter();
* const formattedAmount = formatFiat(1000);
* const formattedAmount = formatFiat(new BigNumber(1000));
* ```
*
* @returns A function that takes a fiat amount as a number and returns a formatted string.
*/
const useFiatFormatter = (): FiatFormatter => {
const fiatCurrency = useSelector(selectCurrentCurrency);

return (fiatAmount: number) => {
return (fiatAmount: BigNumber) => {
const hasDecimals = !fiatAmount.isInteger();

try {
return new Intl.NumberFormat(I18n.locale, {
style: 'currency',
currency: fiatCurrency,
}).format(fiatAmount);
minimumFractionDigits: hasDecimals ? 2 : 0,
// string is valid parameter for format function
// for some reason it gives TS issue
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/format#number
}).format(fiatAmount.toFixed() as unknown as number);
} catch (error) {
// Fallback for unknown or unsupported currencies
return `${fiatAmount} ${fiatCurrency}`;
return `${fiatAmount.toFixed()} ${fiatCurrency}`;
}
};
};
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/SimulationDetails/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export enum AssetType {
* Describes an amount of fiat.
*/
export const FIAT_UNAVAILABLE = null;
export type FiatAmountAvailable = number;
export type FiatAmountAvailable = number | string;
export type FiatAmount = FiatAmountAvailable | typeof FIAT_UNAVAILABLE;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('useSimulationMetrics', () => {
const balanceChange = {
...BALANCE_CHANGE_MOCK,
amount: new BigNumber(isNegative ? -1 : 1),
fiatAmount,
fiatAmount: fiatAmount as number,
};

expectUpdateTransactionMetricsCalled(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function getProperties(changes: BalanceChange[], prefix: string) {
function getSensitiveProperties(changes: BalanceChange[], prefix: string) {
const fiatAmounts = changes.map((change) => change.fiatAmount);
const totalFiat = calculateTotalFiat(fiatAmounts);
const totalValue = totalFiat ? Math.abs(totalFiat) : undefined;
const totalValue = totalFiat ? totalFiat.abs().toNumber() : undefined;

return getPrefixProperties({ total_value: totalValue }, prefix);
}
Expand Down
Loading

0 comments on commit 9911443

Please sign in to comment.