Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { renderHook } from '@testing-library/react-hooks';
import { useSelector } from 'react-redux';

import { useAssetFiatFormatter } from './useAssetFiatFormatter';
import { useTransactionPayCurrency } from './useTransactionPayCurrency';
import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
import { getIntlNumberFormatter } from '../../../../../util/intl';

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

jest.mock('./useTransactionPayCurrency', () => ({
useTransactionPayCurrency: jest.fn(),
}));

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

jest.mock('../../../../../util/intl', () => ({
getIntlNumberFormatter: jest.fn(),
}));

jest.mock('../../../../../../locales/i18n', () => ({
locale: 'en-US',
strings: jest.fn((key: string) => key),
}));

const mockUseSelector = jest.mocked(useSelector);
const mockUseTransactionPayCurrency = jest.mocked(useTransactionPayCurrency);
const mockGetIntlNumberFormatter = jest.mocked(getIntlNumberFormatter);

const mockFormatter = { format: jest.fn() };

function buildState({ preferredCurrency = 'usd' } = {}) {
mockUseSelector.mockImplementation((selector) => {
if (selector === selectCurrentCurrency) return preferredCurrency;
return undefined;
});
}

describe('useAssetFiatFormatter', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseTransactionPayCurrency.mockReturnValue(undefined);
mockGetIntlNumberFormatter.mockReturnValue(
mockFormatter as unknown as ReturnType<typeof getIntlNumberFormatter>,
);
mockFormatter.format.mockImplementation((n) => `formatted:${String(n)}`);
});

describe('fiatCurrency selection', () => {
it('uses the preferred currency outside pay flow', () => {
buildState({ preferredCurrency: 'eur' });

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

expect(result.current.fiatCurrency).toBe('eur');
});

it('uses the pay currency when useTransactionPayCurrency returns a value', () => {
buildState({ preferredCurrency: 'eur' });
mockUseTransactionPayCurrency.mockReturnValue('USD');

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

expect(result.current.fiatCurrency).toBe('USD');
});
});

describe('format', () => {
it('formats a numeric amount using Intl currency', () => {
buildState({ preferredCurrency: 'eur' });

const { result } = renderHook(() => useAssetFiatFormatter());
const output = result.current.format('100');

expect(mockGetIntlNumberFormatter).toHaveBeenCalledWith(
'en-US',
expect.objectContaining({ style: 'currency', currency: 'eur' }),
);
expect(mockFormatter.format).toHaveBeenCalledWith('100');
expect(output).toBe('formatted:100');
});

it('formats with the pay currency when in pay flow', () => {
buildState({ preferredCurrency: 'eur' });
mockUseTransactionPayCurrency.mockReturnValue('USD');

const { result } = renderHook(() => useAssetFiatFormatter());
result.current.format('110');

expect(mockGetIntlNumberFormatter).toHaveBeenCalledWith(
'en-US',
expect.objectContaining({ currency: 'USD' }),
);
expect(mockFormatter.format).toHaveBeenCalledWith('110');
});

it('uses minimumFractionDigits=0 for integer amounts', () => {
buildState({ preferredCurrency: 'eur' });

renderHook(() => useAssetFiatFormatter()).result.current.format('100');

expect(mockGetIntlNumberFormatter).toHaveBeenCalledWith(
'en-US',
expect.objectContaining({ minimumFractionDigits: 0 }),
);
});

it('uses minimumFractionDigits=2 for non-integer amounts', () => {
buildState({ preferredCurrency: 'eur' });

renderHook(() => useAssetFiatFormatter()).result.current.format('100.5');

expect(mockGetIntlNumberFormatter).toHaveBeenCalledWith(
'en-US',
expect.objectContaining({ minimumFractionDigits: 2 }),
);
});

it('returns undefined when input is undefined', () => {
buildState({ preferredCurrency: 'eur' });

const { result } = renderHook(() => useAssetFiatFormatter());
const output = result.current.format(undefined);

expect(output).toBeUndefined();
expect(mockFormatter.format).not.toHaveBeenCalled();
});

it('falls back to `${value} ${currency}` when Intl throws', () => {
buildState({ preferredCurrency: 'eur' });
mockGetIntlNumberFormatter.mockImplementation(() => {
throw new Error('boom');
});

const { result } = renderHook(() => useAssetFiatFormatter());
const output = result.current.format('42');

expect(output).toBe('42 eur');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useSelector } from 'react-redux';
import { useCallback } from 'react';
import { BigNumber } from 'bignumber.js';

import I18n from '../../../../../../locales/i18n';
import Logger from '../../../../../util/Logger';
import { getIntlNumberFormatter } from '../../../../../util/intl';
import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
import { useTransactionPayCurrency } from './useTransactionPayCurrency';

export type AssetFiatFormatter = (
amount: BigNumber.Value | undefined,
) => string | undefined;

/**
* Returns a currency formatter whose target currency follows the pay-flow
* override: USD inside `PAY_TRANSACTION_TYPES` confirmations, otherwise the
* user's preferred currency.
*
* This hook only formats — it does not derive rates or convert values.
* Callers must pass an `amount` already denominated in `fiatCurrency`. Rate
* derivation should go through `useTokenFiatRates`, which owns the
* stablecoin exception and the USD/preferred-currency rate selection.
*
* Returns `undefined` when the input amount is `undefined`, so callers can
* pass through a missing rate as "hide fiat".
*/
export function useAssetFiatFormatter(): {
format: AssetFiatFormatter;
fiatCurrency: string;
} {
const preferredCurrency = useSelector(selectCurrentCurrency);
const payCurrency = useTransactionPayCurrency();
const fiatCurrency = payCurrency ?? preferredCurrency;

const format = useCallback<AssetFiatFormatter>(
(amount) => {
if (amount === undefined) {
return undefined;
}

const value = new BigNumber(amount);
const hasDecimals = !value.isInteger();

try {
return getIntlNumberFormatter(I18n.locale, {
style: 'currency',
currency: fiatCurrency,
minimumFractionDigits: hasDecimals ? 2 : 0,
}).format(value.toFixed() as unknown as number);
} catch (error) {
Logger.error(error as Error);
return `${value.toFixed()} ${fiatCurrency}`;
}
},
[fiatCurrency],
);

return { format, fiatCurrency };
}
Loading
Loading