Skip to content

Commit

Permalink
Merge pull request #8299 from mdneyazahmad/fix/7915-localize-currency…
Browse files Browse the repository at this point in the history
…-symbol

[Fix] Localize currency symbol
  • Loading branch information
mountiny committed Jun 6, 2022
2 parents d0a9887 + d87f569 commit 52c7907
Show file tree
Hide file tree
Showing 8 changed files with 1,113 additions and 42 deletions.
52 changes: 52 additions & 0 deletions src/components/AmountTextInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import TextInput from './TextInput';
import styles from '../styles/styles';
import CONST from '../CONST';

const propTypes = {
/** Formatted amount in local currency */
formattedAmount: PropTypes.string.isRequired,

/** A ref to forward to amount text input */
forwardedRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
]),

/** Function to call when amount in text input is changed */
onChangeAmount: PropTypes.func.isRequired,

/** Placeholder value for amount text input */
placeholder: PropTypes.string.isRequired,
};

const defaultProps = {
forwardedRef: undefined,
};

function AmountTextInput(props) {
return (
<TextInput
disableKeyboard
autoGrow
hideFocusedState
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
onChangeText={props.onChangeAmount}
ref={props.forwardedRef}
value={props.formattedAmount}
placeholder={props.placeholder}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
/>
);
}

AmountTextInput.propTypes = propTypes;
AmountTextInput.defaultProps = defaultProps;
AmountTextInput.displayName = 'AmountTextInput';

export default React.forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<AmountTextInput {...props} forwardedRef={ref} />
));
26 changes: 26 additions & 0 deletions src/components/CurrencySymbolButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import {TouchableOpacity} from 'react-native';
import PropTypes from 'prop-types';
import Text from './Text';
import styles from '../styles/styles';

const propTypes = {
/** Currency symbol of selected currency */
currencySymbol: PropTypes.string.isRequired,

/** Function to call when currency button is pressed */
onCurrencyButtonPress: PropTypes.func.isRequired,
};

function CurrencySymbolButton(props) {
return (
<TouchableOpacity onPress={props.onCurrencyButtonPress}>
<Text style={styles.iouAmountText}>{props.currencySymbol}</Text>
</TouchableOpacity>
);
}

CurrencySymbolButton.propTypes = propTypes;
CurrencySymbolButton.displayName = 'CurrencySymbolButton';

export default CurrencySymbolButton;
83 changes: 83 additions & 0 deletions src/components/TextInputWithCurrencySymbol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import AmountTextInput from './AmountTextInput';
import CurrencySymbolButton from './CurrencySymbolButton';
import * as CurrencySymbolUtils from '../libs/CurrencySymbolUtils';

const propTypes = {
/** A ref to forward to amount text input */
forwardedRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
]),

/** Formatted amount in local currency */
formattedAmount: PropTypes.string.isRequired,

/** Function to call when amount in text input is changed */
onChangeAmount: PropTypes.func,

/** Function to call when currency button is pressed */
onCurrencyButtonPress: PropTypes.func,

/** Placeholder value for amount text input */
placeholder: PropTypes.string.isRequired,

/** Preferred locale of the user */
preferredLocale: PropTypes.string.isRequired,

/** Currency code of user's selected currency */
selectedCurrencyCode: PropTypes.string.isRequired,
};

const defaultProps = {
forwardedRef: undefined,
onChangeAmount: () => {},
onCurrencyButtonPress: () => {},
};

function TextInputWithCurrencySymbol(props) {
const currencySymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(props.preferredLocale, props.selectedCurrencyCode);
const isCurrencySymbolLTR = CurrencySymbolUtils.isCurrencySymbolLTR(props.preferredLocale, props.selectedCurrencyCode);

const currencySymbolButton = (
<CurrencySymbolButton
currencySymbol={currencySymbol}
onCurrencyButtonPress={props.onCurrencyButtonPress}
/>
);

const amountTextInput = (
<AmountTextInput
formattedAmount={props.formattedAmount}
onChangeAmount={props.onChangeAmount}
placeholder={props.placeholder}
ref={props.forwardedRef}
/>
);

if (isCurrencySymbolLTR) {
return (
<>
{currencySymbolButton}
{amountTextInput}
</>
);
}

return (
<>
{amountTextInput}
{currencySymbolButton}
</>
);
}

TextInputWithCurrencySymbol.propTypes = propTypes;
TextInputWithCurrencySymbol.defaultProps = defaultProps;
TextInputWithCurrencySymbol.displayName = 'TextInputWithCurrencySymbol';

export default React.forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<TextInputWithCurrencySymbol {...props} forwardedRef={ref} />
));
37 changes: 37 additions & 0 deletions src/libs/CurrencySymbolUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import _ from 'underscore';
import * as NumberFormatUtils from './NumberFormatUtils';

/**
* Get localized currency symbol for currency(ISO 4217) Code
* @param {String} preferredLocale
* @param {String} currencyCode
* @returns {String}
*/
function getLocalizedCurrencySymbol(preferredLocale, currencyCode) {
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, {
style: 'currency',
currency: currencyCode,
});
return _.find(parts, part => part.type === 'currency').value;
}

/**
* Whether the currency symbol is left-to-right.
* @param {String} preferredLocale
* @param {String} currencyCode
* @returns {Boolean}
*/
function isCurrencySymbolLTR(preferredLocale, currencyCode) {
const parts = NumberFormatUtils.formatToParts(preferredLocale, 0, {
style: 'currency',
currency: currencyCode,
});

// Currency is LTR when the first part is of currency type.
return parts[0].type === 'currency';
}

export {
getLocalizedCurrencySymbol,
isCurrencySymbolLTR,
};
3 changes: 2 additions & 1 deletion src/pages/iou/IOUCurrencySelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
import Button from '../../components/Button';
import FixedFooter from '../../components/FixedFooter';
import * as IOU from '../../libs/actions/IOU';
import * as CurrencySymbolUtils from '../../libs/CurrencySymbolUtils';
import {withNetwork} from '../../components/OnyxProvider';
import networkPropTypes from '../../components/networkPropTypes';

Expand Down Expand Up @@ -127,7 +128,7 @@ class IOUCurrencySelection extends Component {
getCurrencyOptions() {
const currencyListKeys = _.keys(this.props.currencyList);
const currencyOptions = _.map(currencyListKeys, currencyCode => ({
text: `${currencyCode} - ${this.props.currencyList[currencyCode].symbol}`,
text: `${currencyCode} - ${CurrencySymbolUtils.getLocalizedCurrencySymbol(this.props.preferredLocale, currencyCode)}`,
searchText: `${currencyCode} ${this.props.currencyList[currencyCode].symbol}`,
currencyCode,
}));
Expand Down
61 changes: 20 additions & 41 deletions src/pages/iou/steps/IOUAmountPage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import {
View,
TouchableOpacity,
InteractionManager,
} from 'react-native';
import PropTypes from 'prop-types';
Expand All @@ -16,10 +15,9 @@ import ROUTES from '../../../ROUTES';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import compose from '../../../libs/compose';
import Button from '../../../components/Button';
import Text from '../../../components/Text';
import CONST from '../../../CONST';
import TextInput from '../../../components/TextInput';
import canUseTouchScreen from '../../../libs/canUseTouchscreen';
import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';

const propTypes = {
/** Whether or not this IOU has multiple participants */
Expand All @@ -31,18 +29,6 @@ const propTypes = {
/** Callback to inform parent modal of success */
onStepComplete: PropTypes.func.isRequired,

/** The currency list constant object from Onyx */
currencyList: PropTypes.objectOf(PropTypes.shape({
/** Symbol for the currency */
symbol: PropTypes.string,

/** Name of the currency */
name: PropTypes.string,

/** ISO4217 Code for the currency */
ISO4217: PropTypes.string,
})).isRequired,

/** Previously selected amount to show if the user comes back to this screen */
selectedAmount: PropTypes.string.isRequired,

Expand Down Expand Up @@ -75,6 +61,7 @@ class IOUAmountPage extends React.Component {
this.updateAmount = this.updateAmount.bind(this);
this.stripCommaFromAmount = this.stripCommaFromAmount.bind(this);
this.focusTextInput = this.focusTextInput.bind(this);
this.navigateToCurrencySelectionPage = this.navigateToCurrencySelectionPage.bind(this);

this.state = {
amount: props.selectedAmount,
Expand Down Expand Up @@ -205,8 +192,19 @@ class IOUAmountPage extends React.Component {
.value();
}

navigateToCurrencySelectionPage() {
if (this.props.hasMultipleParticipants) {
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID));
}
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) {
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID));
}
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID));
}

render() {
const formattedAmount = this.replaceAllDigits(this.state.amount, this.props.toLocaleDigit);

return (
<>
<View style={[
Expand All @@ -217,32 +215,14 @@ class IOUAmountPage extends React.Component {
styles.justifyContentCenter,
]}
>
<TouchableOpacity onPress={() => {
if (this.props.hasMultipleParticipants) {
return Navigation.navigate(ROUTES.getIouBillCurrencyRoute(this.props.reportID));
}
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) {
return Navigation.navigate(ROUTES.getIouSendCurrencyRoute(this.props.reportID));
}
return Navigation.navigate(ROUTES.getIouRequestCurrencyRoute(this.props.reportID));
}}
>
<Text style={styles.iouAmountText}>
{lodashGet(this.props.currencyList, [this.props.iou.selectedCurrencyCode, 'symbol'])}
</Text>
</TouchableOpacity>
<TextInput
disableKeyboard
autoGrow
hideFocusedState
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
onChangeText={this.updateAmount}
ref={el => this.textInput = el}
value={formattedAmount}
<TextInputWithCurrencySymbol
formattedAmount={formattedAmount}
onChangeAmount={this.updateAmount}
onCurrencyButtonPress={this.navigateToCurrencySelectionPage}
placeholder={this.props.numberFormat(0)}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
blurOnSubmit={false}
preferredLocale={this.props.preferredLocale}
ref={el => this.textInput = el}
selectedCurrencyCode={this.props.iou.selectedCurrencyCode}
/>
</View>
<View style={[styles.w100, styles.justifyContentEnd]}>
Expand Down Expand Up @@ -273,7 +253,6 @@ IOUAmountPage.defaultProps = defaultProps;
export default compose(
withLocalize,
withOnyx({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
iou: {key: ONYXKEYS.IOU},
}),
)(IOUAmountPage);
38 changes: 38 additions & 0 deletions tests/unit/CurrencySymbolUtilsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import _ from 'underscore';
import * as CurrencySymbolUtils from '../../src/libs/CurrencySymbolUtils';

// This file can get outdated. In that case, you can follow these steps to update it:
// - in src/libs/API.js
// - call: GetCurrencyList().then(data => console.log(data.currencyList));
// - copy the json from console and format it to valid json using some external tool
// - update currencyList.json
import currencyList from './currencyList.json';

const currencyCodeList = _.keys(currencyList);
const AVAILABLE_LOCALES = ['en', 'es'];

// Contains item [isLeft, locale, currencyCode]
const symbolPositions = [
[true, 'en', 'USD'],
[false, 'es', 'USD'],
];

describe('CurrencySymbolUtils', () => {
describe('getLocalizedCurrencySymbol', () => {
test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => {
_.forEach(currencyCodeList, (currencyCode) => {
const localizedSymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(prefrredLocale, currencyCode);

expect(localizedSymbol).toBeTruthy();
});
});
});

describe('isCurrencySymbolLTR', () => {
test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => {
const isSymbolLeft = CurrencySymbolUtils.isCurrencySymbolLTR(locale, currencyCode);
expect(isSymbolLeft).toBe(isLeft);
});
});
});

Loading

0 comments on commit 52c7907

Please sign in to comment.