Skip to content

Commit e57cbc0

Browse files
feat(Amount): Negative Amount support (#2320)
* fix: add negative sign to amount * fix: negative numbers for amount * feat: remove formatted * refactor: remove formatNumberByParts extra instances * feat: remove formatted number * fix: tests for node 20 * fix: native amount tests * feat: upgrade i18nify * refactor: remove hardcoded story * fix: failing tests * fix: snapshots * fix: phone number input compatibility with i18nify * feat: snapshots update * fix: CI installation error * fix: snapshots, add more tests for negative amount
1 parent 92648a7 commit e57cbc0

File tree

12 files changed

+553
-137
lines changed

12 files changed

+553
-137
lines changed

packages/blade/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@
280280
"typescript-transform-paths": "3.4.7",
281281
"@types/body-scroll-lock": "3.1.0",
282282
"ramda": "0.29.1",
283-
"@razorpay/i18nify-js": "1.9.3",
284-
"@razorpay/i18nify-react": "4.0.8",
283+
"@razorpay/i18nify-js": "1.12.3",
284+
"@razorpay/i18nify-react": "4.0.12",
285285
"plop": "3.1.1",
286286
"node-plop": "0.32.0",
287287
"svgson": "5.3.1"
@@ -300,8 +300,8 @@
300300
"react-hot-toast": "2.4.1",
301301
"@gorhom/bottom-sheet": "^4.4.6",
302302
"@gorhom/portal": "^1.0.14",
303-
"@razorpay/i18nify-js": "^1.9.3",
304-
"@razorpay/i18nify-react": "^4.0.8"
303+
"@razorpay/i18nify-js": "^1.12.3",
304+
"@razorpay/i18nify-react": "^4.0.12"
305305
},
306306
"peerDependenciesMeta": {
307307
"react-native": {

packages/blade/src/components/Amount/Amount.tsx

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactElement } from 'react';
22
import React from 'react';
33
import type { CurrencyCodeType } from '@razorpay/i18nify-js/currency';
4-
import { formatNumber, formatNumberByParts } from '@razorpay/i18nify-js/currency';
4+
import { formatNumberByParts } from '@razorpay/i18nify-js/currency';
55
import type { AmountTypeProps } from './amountTokens';
66
import { normalAmountSizes, subtleFontSizes, amountLineHeights } from './amountTokens';
77
import type { BaseTextProps } from '~components/Typography/BaseText/types';
@@ -19,6 +19,43 @@ import { Text } from '~components/Typography';
1919
import { opacity } from '~tokens/global';
2020
import type { FontFamily } from '~tokens/global';
2121

22+
/**
23+
* Pollyfill function to get around the node 18 error
24+
*
25+
* This function is maintained by i18nify team. Reach out to them for any change regarding this.
26+
*/
27+
const stripTrailingZerosFromParts = (
28+
parts: ReturnType<typeof formatNumberByParts>,
29+
): ReturnType<typeof formatNumberByParts> => {
30+
const decimalPart = parts.rawParts
31+
.filter(({ type }) => type === 'fraction')
32+
.map(({ value }) => value)
33+
.join('');
34+
35+
const hasFraction = parts.rawParts.some(({ type }) => type === 'fraction');
36+
37+
if (hasFraction && /^0+$/.test(decimalPart)) {
38+
delete parts.decimal;
39+
delete parts.fraction;
40+
parts.rawParts = parts.rawParts.filter(({ type }) => type !== 'decimal' && type !== 'fraction');
41+
}
42+
43+
return parts;
44+
};
45+
46+
/**
47+
* Wrapper that uses pollyfill of i18nify team
48+
*/
49+
const pollyfilledFormatNumberByParts: typeof formatNumberByParts = (value, options) => {
50+
const parts = formatNumberByParts(value, options);
51+
52+
if (options?.intlOptions?.trailingZeroDisplay === 'stripIfInteger') {
53+
return stripTrailingZerosFromParts(parts);
54+
}
55+
56+
return parts;
57+
};
58+
2259
type AmountCommonProps = {
2360
/**
2461
* The value to be rendered within the component.
@@ -82,7 +119,7 @@ const getTextColorProps = ({ color }: { color: AmountProps['color'] }): ColorPro
82119
return props;
83120
};
84121

85-
type AmountType = Partial<ReturnType<typeof formatNumberByParts>> & { formatted: string };
122+
type AmountType = Partial<ReturnType<typeof formatNumberByParts>>;
86123

87124
interface AmountValue extends Omit<AmountProps, 'value'> {
88125
amountValueColor: BaseTextProps['color'];
@@ -141,35 +178,38 @@ const AmountValue = ({
141178
color={amountValueColor}
142179
lineHeight={amountLineHeights[type][size]}
143180
>
144-
{amount.formatted}
181+
{amount.integer}
182+
{amount.decimal}
183+
{amount.fraction}
184+
{amount.compact}
145185
</BaseText>
146186
);
147187
};
148188

149189
type FormatAmountWithSuffixType = {
150190
suffix: AmountProps['suffix'];
151191
value: number;
192+
currency: AmountProps['currency'];
152193
};
153194

154195
/**
155196
* Returns a parsed object based on the suffix passed in parameters
156197
* === Logic ===
157198
* value = 12500.45
158199
* if suffix === 'decimals' => {
159-
"formatted": "12,500.45",
160200
"integer": "12,500",
161201
"decimal": ".",
162202
"fraction": "45",
203+
"compact": "K",
163204
"isPrefixSymbol": false,
164205
"rawParts": [{"type": "integer","value": "12"},{"type": "group","value": ","},{"type": "integer","value": "500"},{"type": "decimal","value": "."},{"type": "fraction","value": "45"}]
165206
}
166-
* else if suffix === 'humanize' => { formatted: "1.2T" }
167-
* else => { formatted: "1,23,456" }
168207
* @returns {AmountType}
169208
*/
170-
export const formatAmountWithSuffix = ({
209+
export const getAmountByParts = ({
171210
suffix,
172211
value,
212+
currency,
173213
}: FormatAmountWithSuffixType): AmountType => {
174214
try {
175215
switch (suffix) {
@@ -179,40 +219,37 @@ export const formatAmountWithSuffix = ({
179219
maximumFractionDigits: 2,
180220
minimumFractionDigits: 2,
181221
},
182-
};
183-
return {
184-
...formatNumberByParts(value, options),
185-
formatted: formatNumber(value, options),
186-
};
222+
currency,
223+
} as const;
224+
return pollyfilledFormatNumberByParts(value, options);
187225
}
188226
case 'humanize': {
189-
const formatted = formatNumber(value, {
227+
const options = {
190228
intlOptions: {
191229
notation: 'compact',
192230
maximumFractionDigits: 2,
193231
trailingZeroDisplay: 'stripIfInteger',
194232
},
195-
});
196-
return {
197-
formatted,
198-
};
233+
currency,
234+
} as const;
235+
return pollyfilledFormatNumberByParts(value, options);
199236
}
200237

201238
default: {
202-
const formatted = formatNumber(value, {
239+
const options = {
203240
intlOptions: {
204241
maximumFractionDigits: 0,
205242
roundingMode: 'floor',
206243
},
207-
});
208-
return {
209-
formatted,
210-
};
244+
currency,
245+
} as const;
246+
return pollyfilledFormatNumberByParts(value, options);
211247
}
212248
}
213249
} catch (err: unknown) {
214250
return {
215-
formatted: `${value}`,
251+
integer: `${value}`,
252+
currency,
216253
};
217254
}
218255
};
@@ -275,20 +312,11 @@ const _Amount = ({
275312
color,
276313
});
277314

278-
let isPrefixSymbol, currencySymbol;
279-
try {
280-
const byParts = formatNumberByParts(value, {
281-
currency,
282-
});
283-
isPrefixSymbol = byParts.isPrefixSymbol;
284-
currencySymbol = byParts.currency;
285-
} catch (err: unknown) {
286-
isPrefixSymbol = true;
287-
currencySymbol = currency;
288-
}
315+
const renderedValue = getAmountByParts({ suffix, value, currency });
316+
const isPrefixSymbol = renderedValue.isPrefixSymbol ?? true;
317+
const currencySymbol = renderedValue.currency ?? currency;
289318

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

294322
const currencyFontSize = isAffixSubtle
@@ -309,6 +337,18 @@ const _Amount = ({
309337
flexDirection="row"
310338
position="relative"
311339
>
340+
{renderedValue.minusSign ? (
341+
<BaseText
342+
fontSize={normalAmountSizes[type][size]}
343+
fontWeight={weight}
344+
lineHeight={amountLineHeights[type][size]}
345+
color={amountValueColor}
346+
as={isReactNative ? undefined : 'span'}
347+
marginX="spacing.2"
348+
>
349+
{renderedValue.minusSign}
350+
</BaseText>
351+
) : null}
312352
{currencyPosition === 'left' && (
313353
<BaseText
314354
marginRight="spacing.1"

packages/blade/src/components/Amount/__tests__/Amount.native.test.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { setState } from '@razorpay/i18nify-js';
22
import { I18nProvider } from '@razorpay/i18nify-react';
33
import type { AmountProps } from '../Amount';
4-
import { Amount, formatAmountWithSuffix } from '../Amount';
4+
import { Amount, getAmountByParts } from '../Amount';
55

66
import { AMOUNT_SUFFIX_TEST_SET } from './mock';
77
import renderWithTheme from '~utils/testing/renderWithTheme.native';
@@ -20,6 +20,11 @@ describe('<Amount />', () => {
2020
expect(toJSON()).toMatchSnapshot();
2121
});
2222

23+
it('should render Amount with negative sign', () => {
24+
const { toJSON } = renderWithTheme(<Amount value={-10000} />);
25+
expect(toJSON()).toMatchSnapshot();
26+
});
27+
2328
it('should throw an error when a string is passed', () => {
2429
// @ts-expect-error testing failure case when value is passed as a string
2530
expect(() => renderWithTheme(<Amount value="10000" />)).toThrow(
@@ -119,16 +124,35 @@ describe('<Amount />', () => {
119124

120125
it('should check if formatAmountWithSuffix is returning the right value for humanize decimals and none', () => {
121126
setState({ locale: 'en-IN' });
122-
expect(formatAmountWithSuffix({ value: 1000.22, suffix: 'humanize' })).toEqual({
123-
formatted: '1T',
127+
expect(getAmountByParts({ value: 1000.22, suffix: 'humanize', currency: 'INR' })).toEqual({
128+
compact: 'T',
129+
currency: '₹',
130+
integer: '1',
131+
isPrefixSymbol: true,
132+
rawParts: [
133+
{
134+
type: 'currency',
135+
value: '₹',
136+
},
137+
{
138+
type: 'integer',
139+
value: '1',
140+
},
141+
{
142+
type: 'compact',
143+
value: 'T',
144+
},
145+
],
124146
});
125-
expect(formatAmountWithSuffix({ value: 1000000.0, suffix: 'decimals' })).toEqual({
147+
148+
expect(getAmountByParts({ value: 1000000.0, suffix: 'decimals', currency: 'INR' })).toEqual({
149+
currency: '₹',
126150
decimal: '.',
127-
formatted: '10,00,000.00',
128151
fraction: '00',
129152
integer: '10,00,000',
130-
isPrefixSymbol: false,
153+
isPrefixSymbol: true,
131154
rawParts: [
155+
{ type: 'currency', value: '₹' },
132156
{ type: 'integer', value: '10' },
133157
{ type: 'group', value: ',' },
134158
{ type: 'integer', value: '00' },
@@ -138,41 +162,44 @@ describe('<Amount />', () => {
138162
{ type: 'fraction', value: '00' },
139163
],
140164
});
141-
expect(formatAmountWithSuffix({ value: 10000000, suffix: 'none' })).toEqual({
142-
formatted: '1,00,00,000',
143-
});
165+
expect(getAmountByParts({ value: 10000000, suffix: 'none', currency: 'INR' }).integer).toBe(
166+
'1,00,00,000',
167+
);
144168
// Related issue - https://github.com/razorpay/blade/issues/1572
145-
expect(formatAmountWithSuffix({ value: 2.07, suffix: 'decimals' })).toEqual({
169+
expect(getAmountByParts({ value: 2.07, suffix: 'decimals', currency: 'INR' })).toEqual({
170+
currency: '₹',
146171
decimal: '.',
147-
formatted: '2.07',
148172
fraction: '07',
149173
integer: '2',
150-
isPrefixSymbol: false,
174+
isPrefixSymbol: true,
151175
rawParts: [
176+
{ type: 'currency', value: '₹' },
152177
{ type: 'integer', value: '2' },
153178
{ type: 'decimal', value: '.' },
154179
{ type: 'fraction', value: '07' },
155180
],
156181
});
157-
expect(formatAmountWithSuffix({ value: 2.077, suffix: 'decimals' })).toEqual({
182+
expect(getAmountByParts({ value: 2.077, suffix: 'decimals', currency: 'INR' })).toEqual({
183+
currency: '₹',
158184
decimal: '.',
159-
formatted: '2.08',
160185
fraction: '08',
161186
integer: '2',
162-
isPrefixSymbol: false,
187+
isPrefixSymbol: true,
163188
rawParts: [
189+
{ type: 'currency', value: '₹' },
164190
{ type: 'integer', value: '2' },
165191
{ type: 'decimal', value: '.' },
166192
{ type: 'fraction', value: '08' },
167193
],
168194
});
169-
expect(formatAmountWithSuffix({ value: 2.3, suffix: 'decimals' })).toEqual({
195+
expect(getAmountByParts({ value: 2.3, suffix: 'decimals', currency: 'INR' })).toEqual({
196+
currency: '₹',
170197
decimal: '.',
171-
formatted: '2.30',
172198
fraction: '30',
173199
integer: '2',
174-
isPrefixSymbol: false,
200+
isPrefixSymbol: true,
175201
rawParts: [
202+
{ type: 'currency', value: '₹' },
176203
{ type: 'integer', value: '2' },
177204
{ type: 'decimal', value: '.' },
178205
{ type: 'fraction', value: '30' },

0 commit comments

Comments
 (0)