Currency
Currency configuration
Currency pattern
On our web infrastructure, the currency is used to identify the currency used for payment with 3-letter code. This code must comply with ISO 4217.
Available currencies
Currencies that are available on our web are fetched from the /v2/locale-currency API.
On the frontend side, we also store the available currency configuration on packages/core/i18n/shared.js as fallback when there is a failure when calling the API. On the same file, there is also a list of experimental currencies (only available on non-production environments).
The list of currencies returned by the API (or the fallback file), is the same as what you would see on the locale/currency picker on our Traveloka web header.
Note: When requesting the list of currencies from the API, we are using an LRU cache with 4 hour time-to-live (TTL) and locale as key.
Extracting currency information
Similar to locales, we have 2 hooks that returns 2 different values, similar to "locale" vs "locales" (one return the current locale/currency, the other return all the supported locales/currencies).
useCurrency
This is used to extract the user's prefered currency (or the default one if they haven't selected any). It returns the CurrencyData object with following types:
type CurrencyData = {
// ISO 4217 currency code
code: string;
// human readable currency name
fullName: string;
// decimal position information
numOfDecimalPoint;
};
Usually this is used for currency formatting purpose
import React from 'react';
import { useCurrency } from '@traveloka/core';
function Component(props) {
const currency = useCurrency();
return <span>User's prefered currency: {locale.code}</span>;
}
useCurrencies
To get the list of all available currencies, we can use the useCurrencies hook. This returns Array<CurrencyData> so you can loop over it.
import React from 'react';
import { useCurrencies } from '@traveloka/core';
function Component() {
const currencies = useCurrencies();
return currencies.map(currency => <span>{currency.code}</span>);
}
Similar with useLocales, this should be rarely used unless you do something related to change currency or currency list. For everything else, using feature control will serve your needs better.
Usage in data-fetching functions
It's exposed as the rawAppContext argument (the second argument of your data-fetching function).
(1) staticProps example
export const getStaticProps = staticProps({
path: '/flight/trpc-playground',
resources: [],
async getStaticProps(ctx, rawAppContext) {
const { currency, currencies } = rawAppContext;
return {};
},
});
(2) getInitialProps example
Page.getInitialProps = async (ctx, rawAppContext) => {
const { currency, currencies } = rawAppContext;
return {};
};
Formatting currency values
We have formatCurrency and useCurrencyFormatter utility to format a raw number into a currency string. Check packages/core/utils/currency-formatter for implementation details.
Configuration
The utility requires configuration settings for each supported currency. Configuration includes (1) the currency symbol, (2) symbol position, (3) number of decimal places, and (4) separator characters. You can see the type definition details in type.ts and the configuration details in config.ts.
To get the configuration of a currency, you can use the getFormatterConfigByCurrencyCode util:
import { getFormatterConfigByCurrencyCode } from '@traveloka/core';
export type CurrencyConfig = {
symbol: string;
symbolPosition: 'head' | 'tail';
numberOfDecimal: number;
isAllowedToBeRounded?: boolean;
separator: {
grouping: string;
decimal: string;
};
};
const currencyConfig: CurrencyConfig = getFormatterConfigByCurrencyCode('IDR');
Adding a new currency
To enable a new currency, plese add it into CurrencyCodes type and configs constants. For example, to enable ZWD (Zimbabwe Dollar) currency, add ZWD on the type definition:
export type CurrencyCodes =
/*
...
Other currency code
...
*/
'ZWD'; /* Zimbabwe Dollar */
Also add the configuration:
export const configs: Configurations = {
/*
...
Other currency config
...
*/
ZWD: {
symbol: 'ZW$',
symbolPosition: 'head',
numberOfDecimal: 2,
separator: {
grouping: ',',
decimal: '.',
},
},
};
Don't forget to update the related unit test located on packages/core/utils/__tests__/CurrencyFormatter.test.ts.
Using formatCurrency
All currencies that can be used in this module only refer to the currencies that have been configured and there is no dependency on the currency list from the backend.
This util is easy to use—just import it. For example:
import { formatCurrency } from '@traveloka/core';
const formattedAmount = formatCurrency(1234, 'IDR', {
displaySign: 'always',
});
Using useCurrencyFormatter
This util is easy to use—just import it. For example:
const formatter = useCurrencyFormatter();
const options = { shorten: true };
// Use user's selected currency
formatter(1234, options);
formatter(1234);
// Use custom currency
formatter(1234, 'USD', options);
formatter(1234, 'USD');
Output
The output of this util will follow the requirements as specified in the SSOT docs.
Parameters
Here are the details about the utility's parameters
| Name | Required | Type | Default value | Description |
|---|---|---|---|---|
amount | yes | number | - | The amount to be formatted without any separators (group or decimal) based on our app configuration (integer). |
currencyCode | yes | string | - | The currency code that defines the formatting rules. If this currency code is not configured, the utility will use default config.
|
options | no | object | - | Additional options that allow you to customize the formatting rules. |
Here are the details about options parameter
| Name | Required | Type | Default value | Description |
|---|---|---|---|---|
displaySign | no | enum | auto | Determines when to display the sign + or - in the formatted amount. always: Always display the sign.never: Never display the sign.auto: Display the sign for negative amounts only. |
hideFraction | no | boolean | false | Determines whether to hide the fraction part of the formatted amount, especially when the number of decimal is not 0. Notes: This property simply hides the decimal part without applying any rounding logic.only |
hideSymbol | no | boolean | false | Determines whether the symbol should be hide |
shouldRoundDecimalValue | no | boolean | false | Determines whether to apply rounding rule for decimal value. Only applied when it's currency code is allowed to be rounded. Rounding Rule: If the decimal part is greater than 49, round up the integer part. Otherwise, remove the decimal part. Notes: The value of the generated currency is guaranteed to be greater than 0. For example, a value of 0.13 will be rounded up to 1, rather than down to 0 |
customConfig | no | object | - | Override related currency code configuration |
Normalizing currency values
Using normalizeCurrencyValue
To store a currency value without losing precision, you can use the normalizeCurrencyValue to store the currency value in BE format (which is an integer).
You can use this integer for in components' internal state, but you might want to use formatCurrency before displaying it back to the user.
import { formatCurrency, normalizeCurrencyValue } from '@traveloka/core';
export default function CurrencySlider(props: Props) {
const { currency, normalizedMinValue, normalizedMaxValue } = props;
const [normalizedValue, setNormalizedValue] = useState<[number]>(value);
const handleMaxInputChange = (val: string) => {
const normalizedValue = normalizeCurrencyValue(val, props.currency);
const clampedValue = Math.min(
Math.max(normalizedValue, normalizedMinValue),
normalizedMaxValue
);
setLocalValue(normalizedValue);
};
const formattedNumber = formatCurrency(normalizedValue, currency, {
hideSymbol: true,
hideFraction: true,
});
return <Slider>{formattedNumber}</Slider>;
}