diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 55a8192f985b..6a0f268ee682 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -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'; @@ -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} diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 412bbdc831bd..3afebd308c14 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -42,7 +42,7 @@ function getCurrencyUnit(currency: string = CONST.CURRENCY.USD): number { } /** - * 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, { @@ -204,6 +204,36 @@ function sanitizeCurrencyCode(currencyCode: string): string { 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 { + 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; @@ -231,4 +261,6 @@ export { isValidCurrencyCode, convertToShortDisplayString, sanitizeCurrencyCode, + getPreferredCurrencySymbol, + isCurrencyCodeLikeSymbol, }; diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 7ce37b8a9a06..9d218370b878 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -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 @@ -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', () => { @@ -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'); + } + + 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(); + }); + }); });