diff --git a/packages/blade/package.json b/packages/blade/package.json index ec3874c136a..a25a1036781 100644 --- a/packages/blade/package.json +++ b/packages/blade/package.json @@ -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" @@ -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": { diff --git a/packages/blade/src/components/Amount/Amount.tsx b/packages/blade/src/components/Amount/Amount.tsx index 33e550eb29f..b60f0bf7b0b 100644 --- a/packages/blade/src/components/Amount/Amount.tsx +++ b/packages/blade/src/components/Amount/Amount.tsx @@ -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'; @@ -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. + */ +const stripTrailingZerosFromParts = ( + parts: ReturnType, +): ReturnType => { + 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) => { + 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. @@ -82,7 +119,7 @@ const getTextColorProps = ({ color }: { color: AmountProps['color'] }): ColorPro return props; }; -type AmountType = Partial> & { formatted: string }; +type AmountType = Partial>; interface AmountValue extends Omit { amountValueColor: BaseTextProps['color']; @@ -141,7 +178,10 @@ const AmountValue = ({ color={amountValueColor} lineHeight={amountLineHeights[type][size]} > - {amount.formatted} + {amount.integer} + {amount.decimal} + {amount.fraction} + {amount.compact} ); }; @@ -149,6 +189,7 @@ const AmountValue = ({ type FormatAmountWithSuffixType = { suffix: AmountProps['suffix']; value: number; + currency: AmountProps['currency']; }; /** @@ -156,20 +197,19 @@ type FormatAmountWithSuffixType = { * === 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) { @@ -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, }; } }; @@ -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 @@ -309,6 +337,18 @@ const _Amount = ({ flexDirection="row" position="relative" > + {renderedValue.minusSign ? ( + + {renderedValue.minusSign} + + ) : null} {currencyPosition === 'left' && ( ', () => { expect(toJSON()).toMatchSnapshot(); }); + it('should render Amount with negative sign', () => { + const { toJSON } = renderWithTheme(); + expect(toJSON()).toMatchSnapshot(); + }); + it('should throw an error when a string is passed', () => { // @ts-expect-error testing failure case when value is passed as a string expect(() => renderWithTheme()).toThrow( @@ -119,16 +124,35 @@ describe('', () => { 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' }, @@ -138,41 +162,44 @@ describe('', () => { { 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' }, diff --git a/packages/blade/src/components/Amount/__tests__/Amount.web.test.tsx b/packages/blade/src/components/Amount/__tests__/Amount.web.test.tsx index 00bbd0c5d8d..3c895ab0097 100644 --- a/packages/blade/src/components/Amount/__tests__/Amount.web.test.tsx +++ b/packages/blade/src/components/Amount/__tests__/Amount.web.test.tsx @@ -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.web'; import assertAccessible from '~utils/testing/assertAccessible.web'; @@ -20,6 +20,11 @@ describe('', () => { expect(container).toMatchSnapshot(); }); + it('should render Amount with negative value', () => { + const { container } = renderWithTheme(); + expect(container).toMatchSnapshot(); + }); + it('should accept testID', () => { const { getByTestId } = renderWithTheme(); @@ -144,16 +149,25 @@ describe('', () => { 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' }, @@ -163,41 +177,44 @@ describe('', () => { { 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' }, @@ -205,8 +222,9 @@ describe('', () => { }); }); - AMOUNT_SUFFIX_TEST_SET.forEach((item) => { - it(`should render ${item.output} in Amount for value:${item.value} & suffix:${item.suffix}`, () => { + it.each(AMOUNT_SUFFIX_TEST_SET)( + `should render different outputs in Amount for different suffix values`, + (item) => { const { getByTestId } = renderWithTheme( ', () => { ); expect(getByTestId('amount-test')).toHaveTextContent(item.output); - }); - }); + }, + ); }); diff --git a/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.native.test.tsx.snap b/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.native.test.tsx.snap index 143dac8d05b..90d9e7fe0a4 100644 --- a/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.native.test.tsx.snap +++ b/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.native.test.tsx.snap @@ -356,6 +356,217 @@ exports[` should render Amount with default props 1`] = ` `; +exports[` should render Amount with negative sign 1`] = ` + + + + + - + + + ₹ + + + + 10,000 + + + . + 00 + + + + + +`; + exports[` should render MYR currency Amount 1`] = ` should render amount with Humanize value 1`] = ` ] } > - 1T + 1 + T @@ -2973,7 +3185,9 @@ exports[` should render information intent Amount 1`] = ` ] } > - 1,000.00 + 1,000 + . + 00 diff --git a/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.web.test.tsx.snap b/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.web.test.tsx.snap index 8ce1964cf34..89bcc9bb527 100644 --- a/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.web.test.tsx.snap +++ b/packages/blade/src/components/Amount/__tests__/__snapshots__/Amount.web.test.tsx.snap @@ -230,6 +230,146 @@ exports[` should render Amount with default props 1`] = ` `; +exports[` should render Amount with negative value 1`] = ` +.c0.c0.c0.c0.c0 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c1.c1.c1.c1.c1 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: baseline; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; + position: relative; +} + +.c2.c2.c2.c2.c2 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + margin-right: 4px; + margin-left: 4px; +} + +.c3.c3.c3.c3.c3 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.625rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + opacity: 0.64; + margin-right: 2px; +} + +.c4.c4.c4.c4.c4 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c5.c5.c5.c5.c5 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.625rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + opacity: 0.64; +} + +
+
+
+ + - + + + ₹ + + + 10,000 + + + . + 00 + +
+
+
+`; + exports[` should render MYR currency Amount 1`] = ` .c0.c0.c0.c0.c0 { display: -webkit-inline-box; @@ -542,7 +682,8 @@ exports[` should render amount with Humanize value 1`] = ` class="c3" data-blade-component="base-text" > - 1K + 1 + K @@ -2055,7 +2196,9 @@ exports[` should render negative intent Amount 1`] = ` class="c3" data-blade-component="base-text" > - 1,000.00 + 1,000 + . + 00 diff --git a/packages/blade/src/components/Amount/__tests__/mock.ts b/packages/blade/src/components/Amount/__tests__/mock.ts index 0af60951011..3e41eb6317e 100644 --- a/packages/blade/src/components/Amount/__tests__/mock.ts +++ b/packages/blade/src/components/Amount/__tests__/mock.ts @@ -6,11 +6,13 @@ export const AMOUNT_SUFFIX_TEST_SET: { output: string; locale?: string; }[] = [ - { value: 1000000.22, suffix: 'humanize', output: '1 Mio.₹', locale: 'de-DE' }, + { value: 1000000.22, suffix: 'humanize', output: '1Mio.₹', locale: 'de-DE' }, { value: 1000000.0, suffix: 'decimals', output: '₹1,000,000.00', locale: 'en-US' }, { value: 10000000, suffix: 'none', output: '₹10,000,000', locale: 'en-US' }, { value: 2.07, suffix: 'decimals', output: '2,07₹', locale: 'fr-FR' }, { value: 2.077, suffix: 'decimals', output: '₹2.08', locale: 'en-IN' }, { value: 2.3, suffix: 'decimals', output: '₹2.30', locale: 'en-IN' }, { value: 1000000.12, suffix: 'decimals', output: '₹10,00,000.12', locale: 'en-IN' }, + { value: -1000000.12, suffix: 'decimals', output: '-₹10,00,000.12', locale: 'en-IN' }, + { value: -1000000.12, suffix: 'humanize', output: '-1Mio.₹', locale: 'de-De' }, ]; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx index 4f25e97f6a0..60b475cdf16 100644 --- a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx +++ b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx @@ -109,6 +109,7 @@ const _PhoneNumberInput: React.ForwardRefRenderFunction !countryCode.includes('-')) // remove the non ISO 3166-1 alpha-2 country codes .map((countryCode) => { return { code: countryCode, diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap index 3ff4d336fc0..a1bdce378ee 100644 --- a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should render 1`] = `"

+91

"`; +exports[` should render 1`] = `"

+91

"`; exports[` should render 2`] = ` .c0.c0.c0.c0.c0 { @@ -601,7 +601,7 @@ exports[` should render 2`] = ` alt="" loading="lazy" role="presentation" - src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/IN.svg" + src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/in.svg" width="20px" />
should render 1`] = ` alt="" loading="lazy" role="presentation" - src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/IN.svg" + src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/in.svg" width="20px" />
should render large size 1`] = ` alt="" loading="lazy" role="presentation" - src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/IN.svg" + src="https://unpkg.com/@razorpay/i18nify-js/lib/assets/flags/in.svg" width="24px" />