Skip to content

fix: hide testnet amounts if user opt out in the settings #25167

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

7 changes: 7 additions & 0 deletions shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,3 +1116,10 @@ export const TEST_NETWORKS = [
LINEA_GOERLI_DISPLAY_NAME,
LINEA_SEPOLIA_DISPLAY_NAME,
];

export const TEST_NETWORK_IDS = [
CHAIN_IDS.GOERLI,
CHAIN_IDS.SEPOLIA,
CHAIN_IDS.LINEA_GOERLI,
CHAIN_IDS.LINEA_SEPOLIA,
];
69 changes: 69 additions & 0 deletions ui/hooks/useHideFiatForTestnet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { renderHook } from '@testing-library/react-hooks';
import { getShowFiatInTestnets, getCurrentChainId } from '../selectors';
import { TEST_NETWORK_IDS, CHAIN_IDS } from '../../shared/constants/network';
import { useHideFiatForTestnet } from './useHideFiatForTestnet';

jest.mock('react-redux', () => ({
useSelector: jest.fn().mockImplementation((selector) => selector()),
}));

jest.mock('../selectors', () => ({
getShowFiatInTestnets: jest.fn(),
getCurrentChainId: jest.fn(),
}));

describe('useHideFiatForTestnet', () => {
const mockGetShowFiatInTestnets = jest.mocked(getShowFiatInTestnets);
const mockGetCurrentChainId = jest.mocked(getCurrentChainId);

beforeEach(() => {
jest.clearAllMocks();
});

it('utilizes the specified chain id', () => {
mockGetShowFiatInTestnets.mockReturnValue(false);
mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]);

const { result } = renderHook(() =>
useHideFiatForTestnet(CHAIN_IDS.MAINNET),
);

expect(result.current).toBe(false);
});

it('returns true if current network is a testnet and showFiatInTestnets is false', () => {
mockGetShowFiatInTestnets.mockReturnValue(false);
mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]);

const { result } = renderHook(() => useHideFiatForTestnet());

expect(result.current).toBe(true);
});

it('returns false if current network is a testnet and showFiatInTestnets is true', () => {
mockGetShowFiatInTestnets.mockReturnValue(true);
mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]);

const { result } = renderHook(() => useHideFiatForTestnet());

expect(result.current).toBe(false);
});

it('returns false if current network is not a testnet', () => {
mockGetShowFiatInTestnets.mockReturnValue(false);
mockGetCurrentChainId.mockReturnValue('1');

const { result } = renderHook(() => useHideFiatForTestnet());

expect(result.current).toBe(false);
});

it('returns false if current network is not a testnet but showFiatInTestnets is true', () => {
mockGetShowFiatInTestnets.mockReturnValue(true);
mockGetCurrentChainId.mockReturnValue('1');

const { result } = renderHook(() => useHideFiatForTestnet());

expect(result.current).toBe(false);
});
});
16 changes: 16 additions & 0 deletions ui/hooks/useHideFiatForTestnet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useSelector } from 'react-redux';
import type { Hex } from '@metamask/utils';
import { getShowFiatInTestnets, getCurrentChainId } from '../selectors';
import { TEST_NETWORK_IDS } from '../../shared/constants/network';

/**
* Returns true if the fiat value should be hidden for testnet networks.
*
* @param providedChainId
* @returns boolean
*/
export const useHideFiatForTestnet = (providedChainId?: Hex): boolean => {
const showFiatInTestnets = useSelector(getShowFiatInTestnets);
const chainId = providedChainId ?? useSelector(getCurrentChainId);
Copy link
Member

Choose a reason for hiding this comment

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

We always have to call useSelector even if we don't use the value, since it's a hook.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

return TEST_NETWORK_IDS.includes(chainId) && !showFiatInTestnets;
};
Original file line number Diff line number Diff line change
@@ -1,47 +1,104 @@
import React from 'react';
import { screen } from '@testing-library/react';
import configureStore from 'redux-mock-store';
import { merge } from 'lodash';
import { useFiatFormatter } from '../../../../hooks/useFiatFormatter';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import mockState from '../../../../../test/data/mock-state.json';
import { CHAIN_IDS } from '../../../../../shared/constants/network';
import { IndividualFiatDisplay, TotalFiatDisplay } from './fiat-display';
import { FIAT_UNAVAILABLE } from './types';

const store = configureStore()(mockState);
const mockStateWithTestnet = merge({}, mockState, {
metamask: {
providerConfig: {
chainId: CHAIN_IDS.SEPOLIA,
Copy link
Member Author

Choose a reason for hiding this comment

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

If we are going with this approach, I believe it's crucial to set some state props like this for the branch of these tests.

Even if mockState is set as using Goerli chain id which is fine for this test, I believe we should still set the stage for only our test. This is the reason of I am creating mockStateWithTestnet

Copy link
Member

Choose a reason for hiding this comment

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

I think extending the default state in the unit tests is a standard pattern.

We also could have defined a CHAIN_ID_MOCK constant using the mockState but this is great also.

},
},
});

jest.mock('../../../../hooks/useFiatFormatter');
(useFiatFormatter as jest.Mock).mockReturnValue((value: number) => `$${value}`);

describe('IndividualFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
[FIAT_UNAVAILABLE, 'Not Available'],
[100, '$100'],
[-100, '$100'],
])(
'when fiatAmount is %s it renders %s',
(fiatAmount: number | null, expected: string) => {
renderWithProvider(
<IndividualFiatDisplay fiatAmount={fiatAmount} />,
store,
);
expect(screen.getByText(expected)).toBeInTheDocument();
const mockStateWithShowingFiatOnTestnets = merge({}, mockStateWithTestnet, {
metamask: {
preferences: {
showFiatInTestnets: true,
},
);
},
});
const mockStoreWithShowingFiatOnTestnets = configureStore()(
mockStateWithShowingFiatOnTestnets,
);

describe('TotalFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
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: (number | null)[], expected: string) => {
renderWithProvider(<TotalFiatDisplay fiatAmounts={fiatAmounts} />, store);
expect(screen.getByText(expected)).toBeInTheDocument();
const mockStateWithHidingFiatOnTestnets = merge({}, mockStateWithTestnet, {
metamask: {
preferences: {
showFiatInTestnets: false,
},
);
},
});
const mockStoreWithHidingFiatOnTestnets = configureStore()(
mockStateWithHidingFiatOnTestnets,
);

jest.mock('../../../../hooks/useFiatFormatter');

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

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

describe('IndividualFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
[FIAT_UNAVAILABLE, 'Not Available'],
[100, '$100'],
[-100, '$100'],
])(
'when fiatAmount is %s it renders %s',
(fiatAmount: number | null, expected: string) => {
renderWithProvider(
<IndividualFiatDisplay fiatAmount={fiatAmount} />,
mockStoreWithShowingFiatOnTestnets,
);
expect(screen.getByText(expected)).toBeInTheDocument();
},
);

it('does not render anything if user opted out to show fiat values on testnet', () => {
const { queryByText } = renderWithProvider(
<IndividualFiatDisplay fiatAmount={100} />,
mockStoreWithHidingFiatOnTestnets,
);
expect(queryByText('100')).toBe(null);
});
});

describe('TotalFiatDisplay', () => {
// @ts-expect-error This is missing from the Mocha type definitions
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: (number | null)[], expected: string) => {
renderWithProvider(
<TotalFiatDisplay fiatAmounts={fiatAmounts} />,
mockStoreWithShowingFiatOnTestnets,
);
expect(screen.getByText(expected)).toBeInTheDocument();
},
);

it('does not render anything if user opted out to show fiat values on testnet', () => {
const { queryByText } = renderWithProvider(
<IndividualFiatDisplay fiatAmount={100} />,
mockStoreWithHidingFiatOnTestnets,
);
expect(queryByText('600')).toBe(null);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useI18nContext } from '../../../../hooks/useI18nContext';
import { Text } from '../../../../components/component-library';
import { SizeNumber } from '../../../../components/component-library/box/box.types';
import { useFiatFormatter } from '../../../../hooks/useFiatFormatter';
import { useHideFiatForTestnet } from '../../../../hooks/useHideFiatForTestnet';
import { FIAT_UNAVAILABLE, FiatAmount } from './types';

const textStyle = {
Expand Down Expand Up @@ -37,7 +38,13 @@ export function calculateTotalFiat(fiatAmounts: FiatAmount[]): number {
export const IndividualFiatDisplay: React.FC<{ fiatAmount: FiatAmount }> = ({
fiatAmount,
}) => {
const hideFiatForTestnet = useHideFiatForTestnet();
const fiatFormatter = useFiatFormatter();

if (hideFiatForTestnet) {
return null;
}

if (fiatAmount === FIAT_UNAVAILABLE) {
return <FiatNotAvailableDisplay />;
}
Expand All @@ -55,10 +62,15 @@ export const IndividualFiatDisplay: React.FC<{ fiatAmount: FiatAmount }> = ({
export const TotalFiatDisplay: React.FC<{
fiatAmounts: FiatAmount[];
}> = ({ fiatAmounts }) => {
const hideFiatForTestnet = useHideFiatForTestnet();
const t = useI18nContext();
const fiatFormatter = useFiatFormatter();
const totalFiat = calculateTotalFiat(fiatAmounts);

if (hideFiatForTestnet) {
return null;
}

return totalFiat === 0 ? (
<FiatNotAvailableDisplay />
) : (
Expand Down