Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/components/MoneyRequestAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useRef} from 'react';
import type {BlurEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import {convertToFrontendAmountAsString, getCurrencyDecimals, getLocalizedCurrencySymbol} from '@libs/CurrencyUtils';
import {convertToFrontendAmountAsString, getCurrencyDecimals, getPreferredCurrencySymbol} from '@libs/CurrencyUtils';
import CONST from '@src/CONST';
import NumberWithSymbolForm from './NumberWithSymbolForm';
import type {NumberWithSymbolFormRef} from './NumberWithSymbolForm';
Expand Down Expand Up @@ -230,7 +230,7 @@ function MoneyRequestAmountInput({
}
numberFormRef.current = newRef;
}}
symbol={getLocalizedCurrencySymbol(preferredLocale, currency) ?? ''}
symbol={getPreferredCurrencySymbol(currency, preferredLocale) ?? ''}
symbolPosition={CONST.TEXT_INPUT_SYMBOL_POSITION.PREFIX}
currency={currency}
hideSymbol={hideCurrencySymbol}
Expand Down
34 changes: 33 additions & 1 deletion src/libs/CurrencyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

let currencyList: OnyxValues[typeof ONYXKEYS.CURRENCY_LIST] = {};

Onyx.connect({

Check warning on line 11 in src/libs/CurrencyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function

Check warning on line 11 in src/libs/CurrencyUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CURRENCY_LIST,
callback: (val) => {
if (!val || Object.keys(val).length === 0) {
Expand Down Expand Up @@ -42,7 +42,7 @@
}

/**
* Get localized currency symbol for currency(ISO 4217) Code
* Get localized currency symbol for currency (ISO 4217) Code
*/
function getLocalizedCurrencySymbol(locale: Locale | undefined, currencyCode: string): string | undefined {
const parts = formatToParts(locale, 0, {
Expand Down Expand Up @@ -204,6 +204,36 @@
return isValidCurrencyCode(currencyCode) ? currencyCode : CONST.CURRENCY.USD;
}

/**
* Checks if a "symbol" is effectively just the ISO currency code (e.g. "TZS").
*/
function isCurrencyCodeLikeSymbol(symbol?: string, currencyCode?: string): boolean {
Comment thread
mountiny marked this conversation as resolved.
if (!symbol || !currencyCode) {
return false;
}

const normalizedSymbol = symbol.trim().toUpperCase();
const normalizedCode = currencyCode.trim().toUpperCase();

return normalizedSymbol === normalizedCode;
}

/**
* Returns a preferred currency symbol for display:
* - Uses the symbol from CURRENCY_LIST when it exists and is not code-like.
* - Otherwise falls back to the localized Intl-derived currency value (existing behavior).
*/
function getPreferredCurrencySymbol(currencyCode: string = CONST.CURRENCY.USD, preferredLocale?: Locale | undefined): string | undefined {
const symbolFromList = getCurrencySymbol(currencyCode);

if (symbolFromList && !isCurrencyCodeLikeSymbol(symbolFromList, currencyCode)) {
return symbolFromList;
}

const locale = preferredLocale ?? IntlStore.getCurrentLocale();
return getLocalizedCurrencySymbol(locale, currencyCode);
}

function getCurrencyKeyByCountryCode(currencies?: CurrencyList, countryCode?: string): string {
if (!currencies || !countryCode) {
return CONST.CURRENCY.USD;
Expand Down Expand Up @@ -231,4 +261,6 @@
isValidCurrencyCode,
convertToShortDisplayString,
sanitizeCurrencyCode,
getPreferredCurrencySymbol,
isCurrencyCodeLikeSymbol,
};
143 changes: 142 additions & 1 deletion tests/unit/CurrencyUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CONST from '@src/CONST';
import IntlStore from '@src/languages/IntlStore';
import * as CurrencyUtils from '@src/libs/CurrencyUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CurrencyList} from '@src/types/onyx';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
// This file can get outdated. In that case, you can follow these steps to update it:
// - open your browser console and navigate to the Network tab
Expand All @@ -12,7 +13,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
// - update currencyList.json
import currencyList from './currencyList.json';

const currencyCodeList = Object.keys(currencyList);
const currencyCodeList = Object.keys(currencyList as CurrencyList);
const AVAILABLE_LOCALES = [CONST.LOCALES.EN, CONST.LOCALES.ES];

describe('CurrencyUtils', () => {
Expand Down Expand Up @@ -189,4 +190,144 @@ describe('CurrencyUtils', () => {
IntlStore.load(CONST.LOCALES.ES).then(() => expect(CurrencyUtils.convertToShortDisplayString(amount, currency)).toBe(expectedResult)),
);
});

describe('isCurrencyCodeLikeSymbol', () => {
test.each([
// Exact match
['USD', 'USD', true],
['ALL', 'ALL', true],

// Case-insensitive
['usd', 'USD', true],
['Usd', 'uSd', true],

// Trimming on both sides
[' usd ', 'USD', true],
['USD', ' usd ', true],
[' usd ', ' UsD ', true],

// Not equal => false
['USD', 'PEN', false],
['US$', 'USD', false],
['USD$', 'USD', false],
['$', 'USD', false],
['S/', 'PEN', false],

// Missing/empty values => false (note: empty string is falsy)
[undefined, 'USD', false],
['USD', undefined, false],
['', 'USD', false],
['USD', '', false],
[' ', 'USD', false], // whitespace becomes '' after trim, but it's still falsy upfront
['USD', ' ', false],
])('symbol=%s, currencyCode=%s => %s', (symbol, currencyCode, expected) => {
expect(CurrencyUtils.isCurrencyCodeLikeSymbol(symbol, currencyCode)).toBe(expected);
});
});

describe('getPreferredCurrencySymbol', () => {
test('Uses CURRENCY_LIST.symbol when it exists and is not code-like', () => {
const preferredLocale = CONST.LOCALES.EN;
const currencyListTyped = currencyList as CurrencyList;
const currencyWithNonCodeLikeSymbol = currencyCodeList.find((code) => {
const symbol = currencyListTyped?.[code]?.symbol;
return !!symbol && symbol.trim().toUpperCase() !== code.trim().toUpperCase();
});

expect(currencyWithNonCodeLikeSymbol).toBeTruthy();

if (!currencyWithNonCodeLikeSymbol) {
throw new Error('Expected a currency with a non-code-like symbol');
}
Comment thread
mountiny marked this conversation as resolved.

const expectedSymbol = CurrencyUtils.getCurrencySymbol(currencyWithNonCodeLikeSymbol);
expect(expectedSymbol).toBeTruthy();

expect(CurrencyUtils.getPreferredCurrencySymbol(currencyWithNonCodeLikeSymbol, preferredLocale)).toBe(expectedSymbol);
});

test('Falls back to localized value when CURRENCY_LIST.symbol is code-like', async () => {
const preferredLocale = CONST.LOCALES.EN;

await IntlStore.load(preferredLocale);
await waitForBatchedUpdates();

// Force a known case: make the USD symbol "USD" so it becomes code-like
const modifiedCurrencyList = {
...currencyList,
USD: {
...currencyList.USD,
symbol: 'USD',
},
};

await Onyx.set(ONYXKEYS.CURRENCY_LIST, modifiedCurrencyList);
await waitForBatchedUpdates();

const preferred = CurrencyUtils.getPreferredCurrencySymbol(CONST.CURRENCY.USD, preferredLocale);
const localized = CurrencyUtils.getLocalizedCurrencySymbol(preferredLocale, CONST.CURRENCY.USD);

expect(preferred).toBe(localized);
expect(preferred).not.toBe('USD');

// Restore for the rest of the suite
await Onyx.set(ONYXKEYS.CURRENCY_LIST, currencyList);
await waitForBatchedUpdates();
});

test('Falls back to localized value when CURRENCY_LIST.symbol is missing', async () => {
const preferredLocale = CONST.LOCALES.EN;

await IntlStore.load(preferredLocale);
await waitForBatchedUpdates();

const modifiedCurrencyList = {
...currencyList,
USD: {
...currencyList.USD,
symbol: undefined,
},
};

await Onyx.set(ONYXKEYS.CURRENCY_LIST, modifiedCurrencyList);
await waitForBatchedUpdates();

const preferred = CurrencyUtils.getPreferredCurrencySymbol(CONST.CURRENCY.USD, preferredLocale);
const localized = CurrencyUtils.getLocalizedCurrencySymbol(preferredLocale, CONST.CURRENCY.USD);

expect(preferred).toBe(localized);
expect(preferred).toBeTruthy();

// Restore for the rest of the suite
await Onyx.set(ONYXKEYS.CURRENCY_LIST, currencyList);
await waitForBatchedUpdates();
});

test.each([['USD'], [' usd '], ['Usd']])('Falls back to localized value when CURRENCY_LIST.symbol is code-like (%s)', async (codeLikeSymbol) => {
const preferredLocale = CONST.LOCALES.EN;

await IntlStore.load(preferredLocale);
await waitForBatchedUpdates();

const modifiedCurrencyList = {
...currencyList,
USD: {
...currencyList.USD,
symbol: codeLikeSymbol,
},
};

await Onyx.set(ONYXKEYS.CURRENCY_LIST, modifiedCurrencyList);
await waitForBatchedUpdates();

const preferred = CurrencyUtils.getPreferredCurrencySymbol(CONST.CURRENCY.USD, preferredLocale);
const localized = CurrencyUtils.getLocalizedCurrencySymbol(preferredLocale, CONST.CURRENCY.USD);

expect(preferred).toBe(localized);
expect(preferred).not.toBe(codeLikeSymbol);

await Onyx.set(ONYXKEYS.CURRENCY_LIST, currencyList);
await waitForBatchedUpdates();
});
});
});
Loading