Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Amount): Negative Amount support #2320

Merged
merged 16 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
8 changes: 4 additions & 4 deletions packages/blade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@
"typescript-transform-paths": "3.4.7",
"@types/body-scroll-lock": "3.1.0",
"ramda": "0.29.1",
"@razorpay/i18nify-js": "1.9.3",
"@razorpay/i18nify-react": "4.0.8",
"@razorpay/i18nify-js": "1.12.3",
"@razorpay/i18nify-react": "4.0.12",
"plop": "3.1.1",
"node-plop": "0.32.0",
"svgson": "5.3.1"
Expand All @@ -300,8 +300,8 @@
"react-hot-toast": "2.4.1",
"@gorhom/bottom-sheet": "^4.4.6",
"@gorhom/portal": "^1.0.14",
"@razorpay/i18nify-js": "^1.9.3",
"@razorpay/i18nify-react": "^4.0.8"
"@razorpay/i18nify-js": "^1.12.3",
"@razorpay/i18nify-react": "^4.0.12"
},
"peerDependenciesMeta": {
"react-native": {
Expand Down
110 changes: 75 additions & 35 deletions packages/blade/src/components/Amount/Amount.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { CurrencyCodeType } from '@razorpay/i18nify-js/currency';
import { formatNumber, formatNumberByParts } from '@razorpay/i18nify-js/currency';
import { formatNumberByParts } from '@razorpay/i18nify-js/currency';
import type { AmountTypeProps } from './amountTokens';
import { normalAmountSizes, subtleFontSizes, amountLineHeights } from './amountTokens';
import type { BaseTextProps } from '~components/Typography/BaseText/types';
Expand All @@ -19,6 +19,43 @@ import { Text } from '~components/Typography';
import { opacity } from '~tokens/global';
import type { FontFamily } from '~tokens/global';

/**
* Pollyfill function to get around the node 18 error
*
* This function is maintained by i18nify team. Reach out to them for any change regarding this.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this come from i18nify library itself then instead of being in blade repo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did suggest them this but they are saying they'll just document it on their end in documentation and consumers will have to add it. Can discuss it later separately as well. Wanted to get it resolved earlier for negative amount thing. But yup overall I too feel this should be in their repo

*/
const stripTrailingZerosFromParts = (
parts: ReturnType<typeof formatNumberByParts>,
): ReturnType<typeof formatNumberByParts> => {
const decimalPart = parts.rawParts
.filter(({ type }) => type === 'fraction')
.map(({ value }) => value)
.join('');

const hasFraction = parts.rawParts.some(({ type }) => type === 'fraction');

if (hasFraction && /^0+$/.test(decimalPart)) {
delete parts.decimal;
delete parts.fraction;
parts.rawParts = parts.rawParts.filter(({ type }) => type !== 'decimal' && type !== 'fraction');
}

return parts;
};

/**
* Wrapper that uses pollyfill of i18nify team
*/
const pollyfilledFormatNumberByParts: typeof formatNumberByParts = (value, options) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also since this is a polyfill are we conditionally calling it or are we always calling it regardless.

eg: checking for formatToParts existence

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put conditions inside this function itself. So we can always call this instead of formatNumberByParts. formatToParts is supported in most major browsers we target so its fine to not check that

const parts = formatNumberByParts(value, options);

if (options?.intlOptions?.trailingZeroDisplay === 'stripIfInteger') {
return stripTrailingZerosFromParts(parts);
}

return parts;
};

type AmountCommonProps = {
/**
* The value to be rendered within the component.
Expand Down Expand Up @@ -82,7 +119,7 @@ const getTextColorProps = ({ color }: { color: AmountProps['color'] }): ColorPro
return props;
};

type AmountType = Partial<ReturnType<typeof formatNumberByParts>> & { formatted: string };
type AmountType = Partial<ReturnType<typeof formatNumberByParts>>;

interface AmountValue extends Omit<AmountProps, 'value'> {
amountValueColor: BaseTextProps['color'];
Expand Down Expand Up @@ -141,35 +178,38 @@ const AmountValue = ({
color={amountValueColor}
lineHeight={amountLineHeights[type][size]}
>
{amount.formatted}
{amount.integer}
{amount.decimal}
{amount.fraction}
{amount.compact}
</BaseText>
);
};

type FormatAmountWithSuffixType = {
suffix: AmountProps['suffix'];
value: number;
currency: AmountProps['currency'];
};

/**
* Returns a parsed object based on the suffix passed in parameters
* === Logic ===
* value = 12500.45
* if suffix === 'decimals' => {
"formatted": "12,500.45",
"integer": "12,500",
"decimal": ".",
"fraction": "45",
"compact": "K",
"isPrefixSymbol": false,
"rawParts": [{"type": "integer","value": "12"},{"type": "group","value": ","},{"type": "integer","value": "500"},{"type": "decimal","value": "."},{"type": "fraction","value": "45"}]
}
* else if suffix === 'humanize' => { formatted: "1.2T" }
* else => { formatted: "1,23,456" }
* @returns {AmountType}
*/
export const formatAmountWithSuffix = ({
export const getAmountByParts = ({
suffix,
value,
currency,
}: FormatAmountWithSuffixType): AmountType => {
try {
switch (suffix) {
Expand All @@ -179,40 +219,37 @@ export const formatAmountWithSuffix = ({
maximumFractionDigits: 2,
minimumFractionDigits: 2,
},
};
return {
...formatNumberByParts(value, options),
formatted: formatNumber(value, options),
};
currency,
} as const;
return pollyfilledFormatNumberByParts(value, options);
}
case 'humanize': {
const formatted = formatNumber(value, {
const options = {
intlOptions: {
notation: 'compact',
maximumFractionDigits: 2,
trailingZeroDisplay: 'stripIfInteger',
},
});
return {
formatted,
};
currency,
} as const;
return pollyfilledFormatNumberByParts(value, options);
}

default: {
const formatted = formatNumber(value, {
const options = {
intlOptions: {
maximumFractionDigits: 0,
roundingMode: 'floor',
},
});
return {
formatted,
};
currency,
} as const;
return pollyfilledFormatNumberByParts(value, options);
}
}
} catch (err: unknown) {
return {
formatted: `${value}`,
integer: `${value}`,
currency,
};
}
};
Expand Down Expand Up @@ -275,20 +312,11 @@ const _Amount = ({
color,
});

let isPrefixSymbol, currencySymbol;
try {
const byParts = formatNumberByParts(value, {
currency,
});
isPrefixSymbol = byParts.isPrefixSymbol;
currencySymbol = byParts.currency;
} catch (err: unknown) {
isPrefixSymbol = true;
currencySymbol = currency;
}
const renderedValue = getAmountByParts({ suffix, value, currency });
const isPrefixSymbol = renderedValue.isPrefixSymbol ?? true;
const currencySymbol = renderedValue.currency ?? currency;

const currencyPosition = isPrefixSymbol ? 'left' : 'right';
const renderedValue = formatAmountWithSuffix({ suffix, value });
const currencySymbolOrCode = currencyIndicator === 'currency-symbol' ? currencySymbol : currency;

const currencyFontSize = isAffixSubtle
Expand All @@ -309,6 +337,18 @@ const _Amount = ({
flexDirection="row"
position="relative"
>
{renderedValue.minusSign ? (
<BaseText
fontSize={normalAmountSizes[type][size]}
fontWeight={weight}
lineHeight={amountLineHeights[type][size]}
color={amountValueColor}
as={isReactNative ? undefined : 'span'}
marginX="spacing.2"
>
{renderedValue.minusSign}
</BaseText>
) : null}
{currencyPosition === 'left' && (
<BaseText
marginRight="spacing.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setState } from '@razorpay/i18nify-js';
import { I18nProvider } from '@razorpay/i18nify-react';
import type { AmountProps } from '../Amount';
import { Amount, formatAmountWithSuffix } from '../Amount';
import { Amount, getAmountByParts } from '../Amount';

import { AMOUNT_SUFFIX_TEST_SET } from './mock';
import renderWithTheme from '~utils/testing/renderWithTheme.native';
Expand Down Expand Up @@ -119,16 +119,35 @@ describe('<Amount />', () => {

it('should check if formatAmountWithSuffix is returning the right value for humanize decimals and none', () => {
setState({ locale: 'en-IN' });
expect(formatAmountWithSuffix({ value: 1000.22, suffix: 'humanize' })).toEqual({
formatted: '1T',
expect(getAmountByParts({ value: 1000.22, suffix: 'humanize', currency: 'INR' })).toEqual({
compact: 'T',
currency: '₹',
integer: '1',
isPrefixSymbol: true,
rawParts: [
{
type: 'currency',
value: '₹',
},
{
type: 'integer',
value: '1',
},
{
type: 'compact',
value: 'T',
},
],
});
expect(formatAmountWithSuffix({ value: 1000000.0, suffix: 'decimals' })).toEqual({

expect(getAmountByParts({ value: 1000000.0, suffix: 'decimals', currency: 'INR' })).toEqual({
currency: '₹',
decimal: '.',
formatted: '10,00,000.00',
fraction: '00',
integer: '10,00,000',
isPrefixSymbol: false,
isPrefixSymbol: true,
rawParts: [
{ type: 'currency', value: '₹' },
{ type: 'integer', value: '10' },
{ type: 'group', value: ',' },
{ type: 'integer', value: '00' },
Expand All @@ -138,41 +157,44 @@ describe('<Amount />', () => {
{ type: 'fraction', value: '00' },
],
});
expect(formatAmountWithSuffix({ value: 10000000, suffix: 'none' })).toEqual({
formatted: '1,00,00,000',
});
expect(getAmountByParts({ value: 10000000, suffix: 'none', currency: 'INR' }).integer).toBe(
'1,00,00,000',
);
// Related issue - https://github.com/razorpay/blade/issues/1572
expect(formatAmountWithSuffix({ value: 2.07, suffix: 'decimals' })).toEqual({
expect(getAmountByParts({ value: 2.07, suffix: 'decimals', currency: 'INR' })).toEqual({
currency: '₹',
decimal: '.',
formatted: '2.07',
fraction: '07',
integer: '2',
isPrefixSymbol: false,
isPrefixSymbol: true,
rawParts: [
{ type: 'currency', value: '₹' },
{ type: 'integer', value: '2' },
{ type: 'decimal', value: '.' },
{ type: 'fraction', value: '07' },
],
});
expect(formatAmountWithSuffix({ value: 2.077, suffix: 'decimals' })).toEqual({
expect(getAmountByParts({ value: 2.077, suffix: 'decimals', currency: 'INR' })).toEqual({
currency: '₹',
decimal: '.',
formatted: '2.08',
fraction: '08',
integer: '2',
isPrefixSymbol: false,
isPrefixSymbol: true,
rawParts: [
{ type: 'currency', value: '₹' },
{ type: 'integer', value: '2' },
{ type: 'decimal', value: '.' },
{ type: 'fraction', value: '08' },
],
});
expect(formatAmountWithSuffix({ value: 2.3, suffix: 'decimals' })).toEqual({
expect(getAmountByParts({ value: 2.3, suffix: 'decimals', currency: 'INR' })).toEqual({
currency: '₹',
decimal: '.',
formatted: '2.30',
fraction: '30',
integer: '2',
isPrefixSymbol: false,
isPrefixSymbol: true,
rawParts: [
{ type: 'currency', value: '₹' },
{ type: 'integer', value: '2' },
{ type: 'decimal', value: '.' },
{ type: 'fraction', value: '30' },
Expand Down
Loading
Loading