Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
38 changes: 32 additions & 6 deletions apps/www/src/content/docs/components/amount/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export const playground = {
groupDigits: {
type: 'checkbox',
defaultValue: true
},
hideCurrency: {
type: 'checkbox',
defaultValue: false
}
},
getCode
Expand Down Expand Up @@ -119,6 +123,17 @@ export const currencyDisplayDemo = {
`
};

export const hideCurrencyDemo = {
type: 'code',
code: `
<Flex gap={4}>
<Amount value={1299} hideCurrency /> {/* 12.99 */}
<Amount value={1299} currency="JPY" hideCurrency /> {/* 1,299 */}
<Amount value={1299} hideCurrency currencyDisplay="code" />{/* 12.99 — currencyDisplay is ignored */}
</Flex>
`
};

export const groupDigitsDemo = {
type: 'code',
code: `
Expand Down Expand Up @@ -149,13 +164,24 @@ export const withTextDemo = {
export const largeNumbersDemo = {
type: 'code',
code: `
<Flex gap={4}>
{/* For large numbers, use string to maintain precision */}
<Flex direction='column' gap={4}>
{/*
For large numbers, use string (supports decimals) or bigint (integer-only)
to maintain precision
*/}
<Amount value="999999999999999" /> {/* $9,999,999,999,999.99 */}
<Amount value="10000100091636935" valueInMinorUnits={false} hideDecimals /> {/* $10,000,100,091,636,935 */}

{/* Numbers exceeding safe integer limit will show warning in console */}
<Amount value={999999999999999} /> {/* Will show warning */}
<Amount value="10000100091636935"
valueInMinorUnits={false} hideDecimals />{/* $10,000,100,091,636,935 */}

{/*
BigInt is always treated as major units — valueInMinorUnits is ignored
*/}
<Amount value={BigInt("9999999999999999999")} valueInMinorUnits={false} />{/* $9,999,999,999,999,999,999.00 */}

{/*
Numbers exceeding safe integer limit will show warning in console
*/}
<Amount value={99999999999999999} />{/* Exceeds Number.MAX_SAFE_INTEGER (~9 × 10^15) — logs a console warning */}
</Flex>
`
};
9 changes: 8 additions & 1 deletion apps/www/src/content/docs/components/amount/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
localeDemo,
hideDecimalsDemo,
currencyDisplayDemo,
hideCurrencyDemo,
groupDigitsDemo,
withTextDemo,
largeNumbersDemo,
Expand Down Expand Up @@ -61,13 +62,19 @@ Formats and displays monetary values with locale and currency support.

<Demo data={currencyDisplayDemo} />

### hideCurrency

Render only the formatted number, without any currency symbol, code, or name. Locale-driven separators and decimal places are preserved.

<Demo data={hideCurrencyDemo} />

### groupDigits

<Demo data={groupDigitsDemo} />

### Large Numbers

For numbers larger than JavaScript's safe integer limit (2^53 - 1), pass the value as a string to maintain precision.
For numbers larger than JavaScript's safe integer limit (2^53 - 1), pass the value as a `string` (supports decimals) or a `bigint` (integer-only). BigInt values are always treated as already in major units, so `valueInMinorUnits` is ignored for them.

<Demo data={largeNumbersDemo} />

Expand Down
22 changes: 18 additions & 4 deletions apps/www/src/content/docs/components/amount/props.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
export interface AmountProps {
/**
* The monetary value to display
* For large numbers (> 2^53), pass the value as string to maintain precision
* The monetary value to display.
* For exact precision beyond 2^53, pass either:
* - a `string` — supports decimals (e.g. "1299" or "12.99")
* - a `bigint` — integer-only; treated as already in major units, so
* `valueInMinorUnits` is ignored when value is a bigint
* @default 0
* @example
* valueInMinorUnits=true: 1299 => "$12.99"
* valueInMinorUnits=false: 12.99 => "$12.99"
* Large numbers: "999999999999999" => "$9,999,999,999,999.99"
* Large strings: "999999999999999" => "$9,999,999,999,999.99"
* BigInt: 1299n => "$1,299.00" (always major units)
*/
value: number | string;
value: number | string | bigint;

/**
* ISO 4217 currency code
Expand Down Expand Up @@ -65,4 +69,14 @@ export interface AmountProps {
* @default true
*/
groupDigits?: boolean;

/**
* Render the formatted number without a currency symbol, code, or name.
* Locale-driven separators, grouping, and fraction digits are preserved.
* When true, `currencyDisplay` is ignored.
* @default false
* @example
* <Amount value={1299} hideCurrency /> => "12.99"
*/
hideCurrency?: boolean;
}
120 changes: 120 additions & 0 deletions packages/raystack/components/amount/__tests__/amount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,125 @@ describe('Amount', () => {
expect(element).toBeInTheDocument();
consoleSpy.mockRestore();
});

it('handles negative string values in minor units', () => {
render(<Amount value='-1299' />);
expect(screen.getByText('-$12.99')).toBeInTheDocument();
});

it('warns when a string longer than 15 digits is formatted', () => {
// The component flags potential precision loss for any >15-digit string,
// letting the developer decide whether their runtime targets are at risk
// and whether to switch to bigint.
const consoleSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => null);
render(<Amount value='10000100091636935' valueInMinorUnits={false} />);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('longer than 15 digits')
);
consoleSpy.mockRestore();
});
});

describe('BigInt support', () => {
it('formats a bigint as major units regardless of valueInMinorUnits', () => {
render(<Amount value={1299n} />);
expect(screen.getByText('$1,299.00')).toBeInTheDocument();
});

it('matches the major-units result when valueInMinorUnits is false', () => {
render(<Amount value={1299n} valueInMinorUnits={false} />);
expect(screen.getByText('$1,299.00')).toBeInTheDocument();
});

it('preserves precision beyond Number.MAX_SAFE_INTEGER', () => {
render(<Amount value={9999999999999999999n} valueInMinorUnits={false} />);
expect(
screen.getByText('$9,999,999,999,999,999,999.00')
).toBeInTheDocument();
});

it('formats negative bigint values', () => {
render(<Amount value={-1299n} />);
expect(screen.getByText('-$1,299.00')).toBeInTheDocument();
});

it('formats bigint with a zero-decimal currency', () => {
render(<Amount value={1299n} currency='JPY' locale='en-US' />);
expect(screen.getByText('¥1,299')).toBeInTheDocument();
});

it('does not warn about safe integer limit for bigint values', () => {
const consoleSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => null);
render(<Amount value={9999999999999999999n} valueInMinorUnits={false} />);
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});

describe('hideCurrency', () => {
it('hides the currency symbol while preserving formatting', () => {
render(<Amount value={1299} hideCurrency />);
expect(screen.getByText('12.99')).toBeInTheDocument();
});

it('preserves trailing zeros for round amounts (USD)', () => {
render(<Amount value={1200} hideCurrency />);
expect(screen.getByText('12.00')).toBeInTheDocument();
});

it('preserves currency fraction digits for round bigint values', () => {
render(<Amount value={1299n} hideCurrency />);
expect(screen.getByText('1,299.00')).toBeInTheDocument();
});

it('preserves currency fraction digits for a 3-decimal currency (BHD)', () => {
render(<Amount value={1234} currency='BHD' hideCurrency />);
expect(screen.getByText('1.234')).toBeInTheDocument();
});

it('overrides currencyDisplay when set', () => {
render(<Amount value={1299} hideCurrency currencyDisplay='code' />);
expect(screen.getByText('12.99')).toBeInTheDocument();
});

it('respects the currency for decimal-place math even when hidden', () => {
render(<Amount value={1299} currency='JPY' hideCurrency />);
expect(screen.getByText('1,299')).toBeInTheDocument();
});

it('honors explicit minimumFractionDigits over the currency default', () => {
render(
<Amount
value={1200}
hideCurrency
minimumFractionDigits={4}
maximumFractionDigits={4}
/>
);
expect(screen.getByText('12.0000')).toBeInTheDocument();
});

it('hideDecimals wins over the currency-default fraction digits', () => {
render(<Amount value={1200} hideCurrency hideDecimals />);
expect(screen.getByText('12')).toBeInTheDocument();
});

it('clamps min when only maximumFractionDigits is provided (avoids RangeError)', () => {
// USD's default min of 2 would invert against max=1; with style:'currency'
// + formatToParts(), Intl's spec auto-clamps min → 1, so 12.99 rounds to 13.0.
render(<Amount value={1299} hideCurrency maximumFractionDigits={1} />);
expect(screen.getByText('13.0')).toBeInTheDocument();
});

it('clamps max when only minimumFractionDigits is provided (avoids RangeError)', () => {
// USD's default max of 2 would invert against min=3; with style:'currency'
// + formatToParts(), Intl's spec auto-clamps max → 3, so 12.99 pads to 12.990.
render(<Amount value={1299} hideCurrency minimumFractionDigits={3} />);
expect(screen.getByText('12.990')).toBeInTheDocument();
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading
Loading