Skip to content

Commit 27e97e3

Browse files
fix (Amount): handle big integer in value prop (#482)
1 parent eba2ea9 commit 27e97e3

File tree

5 files changed

+81
-28
lines changed

5 files changed

+81
-28
lines changed

apps/www/src/app/examples/page.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,12 +1733,9 @@ const Page = () => {
17331733
style={{ marginTop: '32px', marginBottom: '16px' }}
17341734
>
17351735
<Amount
1736-
value={1296367367}
1737-
locale='en-US'
1738-
currency='USD'
1739-
maximumFractionDigits={1}
1740-
currencyDisplay='symbol'
1741-
groupDigits={true}
1736+
value='10000100091636935'
1737+
valueInMinorUnits={false}
1738+
hideDecimals
17421739
/>
17431740
</Text>
17441741

apps/www/src/content/docs/components/amount/demo.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,17 @@ export const withTextDemo = {
145145
</Flex>
146146
`
147147
};
148+
149+
export const largeNumbersDemo = {
150+
type: 'code',
151+
code: `
152+
<Flex gap={4}>
153+
{/* For large numbers, use string to maintain precision */}
154+
<Amount value="999999999999999" /> {/* $9,999,999,999,999.99 */}
155+
<Amount value="10000100091636935" valueInMinorUnits={false} hideDecimals /> {/* $10,000,100,091,636,935 */}
156+
157+
{/* Numbers exceeding safe integer limit will show warning in console */}
158+
<Amount value={999999999999999} /> {/* Will show warning */}
159+
</Flex>
160+
`
161+
};

apps/www/src/content/docs/components/amount/index.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
currencyDisplayDemo,
1515
groupDigitsDemo,
1616
withTextDemo,
17+
largeNumbersDemo,
1718
} from "./demo.ts";
1819

1920
<Demo data={playground} />
@@ -25,6 +26,7 @@ import { Amount } from '@raystack/apsara/v1'
2526

2627
<Amount value={1299} />
2728
<Amount value={1299} currency="EUR" locale="fr-FR" />
29+
<Amount value="999999999999999" />
2830
```
2931

3032
## Amount Props
@@ -61,6 +63,12 @@ import { Amount } from '@raystack/apsara/v1'
6163

6264
<Demo data={groupDigitsDemo} />
6365

66+
### Large Numbers
67+
68+
For numbers larger than JavaScript's safe integer limit (2^53 - 1), pass the value as a string to maintain precision.
69+
70+
<Demo data={largeNumbersDemo} />
71+
6472
### With Text
6573

6674
<Demo data={withTextDemo} />

apps/www/src/content/docs/components/amount/props.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
export interface AmountProps {
22
/**
33
* The monetary value to display
4+
* For large numbers (> 2^53), pass the value as string to maintain precision
45
* @default 0
56
* @example
67
* valueInMinorUnits=true: 1299 => "$12.99"
78
* valueInMinorUnits=false: 12.99 => "$12.99"
9+
* Large numbers: "999999999999999" => "$9,999,999,999,999.99"
810
*/
9-
value: number;
11+
value: number | string;
1012

1113
/**
1214
* ISO 4217 currency code

packages/raystack/components/amount/amount.tsx

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { forwardRef } from 'react';
33
export interface AmountProps {
44
/**
55
* The monetary value to display
6+
* For large numbers (> 2^53), pass the value as string to maintain precision
67
* @default 0
78
* @example
89
* valueInMinorUnits=true: 1299 => "$12.99"
910
* valueInMinorUnits=false: 12.99 => "$12.99"
1011
*/
11-
value: number;
12+
value: number | string;
1213

1314
/**
1415
* ISO 4217 currency code
@@ -155,27 +156,58 @@ export const Amount = forwardRef<HTMLSpanElement, AmountProps>(
155156
},
156157
ref
157158
) => {
158-
const validCurrency = isValidCurrency(currency) ? currency : 'USD';
159-
if (validCurrency !== currency) {
160-
console.warn(`Invalid currency code: ${currency}. Falling back to USD.`);
159+
try {
160+
if (
161+
typeof value === 'number' &&
162+
Math.abs(value) > Number.MAX_SAFE_INTEGER
163+
) {
164+
console.warn(
165+
`Warning: The number ${value} exceeds JavaScript's safe integer limit (${Number.MAX_SAFE_INTEGER}). ` +
166+
'For large numbers, pass the value as a string to maintain precision.'
167+
);
168+
}
169+
170+
const validCurrency = isValidCurrency(currency) ? currency : 'USD';
171+
if (validCurrency !== currency) {
172+
console.warn(
173+
`Invalid currency code: ${currency}. Falling back to USD.`
174+
);
175+
}
176+
177+
const decimals = getCurrencyDecimals(validCurrency);
178+
179+
// Handle minor units - use string manipulation for strings and Math.pow for numbers
180+
const baseValue =
181+
valueInMinorUnits && decimals > 0
182+
? typeof value === 'string'
183+
? value.slice(0, -decimals) + '.' + value.slice(-decimals)
184+
: value / Math.pow(10, decimals)
185+
: value;
186+
187+
// Remove decimals if hideDecimals is true - handle string and number separately
188+
// Note: Not all numbers passed is converted to string as methods like Math.trunc
189+
// or toString cannot handle large numbers thus, we need to handle it separately (large numbers passed in value throws console warning).
190+
const finalBaseValue = hideDecimals
191+
? typeof baseValue === 'string'
192+
? baseValue.split('.')[0]
193+
: Math.trunc(baseValue)
194+
: baseValue;
195+
196+
const formattedValue = new Intl.NumberFormat(locale, {
197+
style: 'currency' as const,
198+
currency: validCurrency.toUpperCase(),
199+
currencyDisplay,
200+
minimumFractionDigits: hideDecimals ? 0 : minimumFractionDigits,
201+
maximumFractionDigits: hideDecimals ? 0 : maximumFractionDigits,
202+
useGrouping: groupDigits
203+
// @ts-ignore - Handling large numbers as string or number, so we need to pass the value as string or number.
204+
}).format(finalBaseValue);
205+
206+
return <span ref={ref}>{formattedValue}</span>;
207+
} catch (error) {
208+
console.error('Error formatting amount:', error);
209+
return <span ref={ref}>{value}</span>;
161210
}
162-
163-
const decimals = getCurrencyDecimals(validCurrency);
164-
const baseValue = valueInMinorUnits
165-
? value / Math.pow(10, decimals)
166-
: value;
167-
const finalBaseValue = hideDecimals ? Math.trunc(baseValue) : baseValue;
168-
169-
const formattedValue = new Intl.NumberFormat(locale, {
170-
style: 'currency' as const,
171-
currency: validCurrency.toUpperCase(),
172-
currencyDisplay,
173-
minimumFractionDigits: hideDecimals ? 0 : minimumFractionDigits,
174-
maximumFractionDigits: hideDecimals ? 0 : maximumFractionDigits,
175-
useGrouping: groupDigits
176-
}).format(finalBaseValue);
177-
178-
return <span ref={ref}>{formattedValue}</span>;
179211
}
180212
);
181213

0 commit comments

Comments
 (0)