Skip to content
7 changes: 7 additions & 0 deletions .changeset/fresh-carrots-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/hydrogen': patch
---

`useMoney` now returns two additional properties: `withoutTraillingZeros` and `withoutTrailingZerosAndCurrency`

`<Money />` now has two additional and optional props: `withoutMoney` and `withoutCurrency`.
2 changes: 2 additions & 0 deletions docs/components/primitive/money.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default function Product() {
| Name | Type | Description |
| ---- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| as? | <code>TTag</code> | An HTML tag to be rendered as the base element wrapper. The default is `div`. |
| withoutCurrency? | <code>boolean</code> | Whether to remove the currency symbol from the output. |
| withoutTrailingZeros? | <code>boolean</code> | Whether to remove trailing zeros (fractional money) from the output. If there are no trailing zeros, then the fractional money amount remains. For example, `$640.00` turns into `$640`. `$640.42` turns into `$640.42`. |
| data | <code>PartialDeep&#60;MoneyV2&#62;</code> | An object with fields that correspond to the Storefront API's [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2). |

## Component type
Expand Down
2 changes: 2 additions & 0 deletions docs/hooks/primitive/usemoney.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ This hook returns an object with the following keys:
| `amount` | The localized amount, without any currency symbols or non-number types from the `Intl.NumberFormat.formatToParts` parts. |
| `parts` | All parts returned by `Intl.NumberFormat.formatToParts`. |
| `original` | The original `MoneyV2` object passed as an argument. |
| `withoutTrailingZeros` | A string with trailing zeros removed from the fractional part, if any exist. If there are no trailing zeros, then the fractional part remains. For example, `$640.00` turns into `$640`. `$640.42` turns into `$640.42`. |
| `withoutTrailingZerosAndCurrency` | A string without currency and without trailing zeros removed from the fractional part, if any exist. If there are no trailing zeros, then the fractional part remains. For example, `$640.00` turns into `640`. `$640.42` turns into `640.42`. |

## Related components

Expand Down
22 changes: 20 additions & 2 deletions packages/hydrogen/src/components/Money/Money.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ interface MoneyProps<TTag> {
as?: TTag;
/** An object with fields that correspond to the Storefront API's [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2). */
data: PartialDeep<MoneyV2>;
/** Whether to remove the currency symbol from the output. */
withoutCurrency?: boolean;
/** Whether to remove trailing zeros (fractional money) from the output. */
withoutTrailingZeros?: boolean;
}

/**
Expand All @@ -18,9 +22,23 @@ interface MoneyProps<TTag> {
export function Money<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: JSX.IntrinsicElements[TTag] & MoneyProps<TTag>
) {
const {data, as, ...passthroughProps} = props;
const {data, as, withoutCurrency, withoutTrailingZeros, ...passthroughProps} =
props;
const moneyObject = useMoney(data);
const Wrapper = as ?? 'div';

return <Wrapper {...passthroughProps}>{moneyObject.localizedString}</Wrapper>;
let output = moneyObject.localizedString;

if (withoutCurrency || withoutTrailingZeros) {
if (withoutCurrency && !withoutTrailingZeros) {
output = moneyObject.amount;
} else if (!withoutCurrency && withoutTrailingZeros) {
output = moneyObject.withoutTrailingZeros;
} else {
// both
output = moneyObject.withoutTrailingZerosAndCurrency;
}
}

return <Wrapper {...passthroughProps}>{output}</Wrapper>;
}
39 changes: 39 additions & 0 deletions packages/hydrogen/src/components/Money/tests/Money.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,43 @@ describe('<Money />', () => {

expect(component).toContainReactComponent(Link, {to: '/test'});
});

it(`removes trailing zeros when the prop is passed`, () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it is verbose, but I suggest specifying the prop (ie: when the withoutTrailingZeros prop is passed)

const money = getPrice({
currencyCode: CurrencyCode.Eur,
amount: '19.00',
});
const component = mountWithProviders(
<Money data={money} withoutTrailingZeros />
);

expect(component).not.toContainReactText(`€${money.amount}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just hard code the 19.00 here.

expect(component).toContainReactText(`€${19}`);
});

it(`removes the currency symbol when the prop is passed`, () => {
const money = getPrice({
currencyCode: CurrencyCode.Eur,
});
const component = mountWithProviders(
<Money data={money} withoutCurrency />
);

expect(component).not.toContainReactText(`€${money.amount}`);
expect(component).toContainReactText(`${money.amount}`);
});

it(`removes the currency symbol and trailing zeros when the props are both passed`, () => {
const money = getPrice({
currencyCode: CurrencyCode.Eur,
amount: '19.00',
});
const component = mountWithProviders(
<Money data={money} withoutCurrency withoutTrailingZeros />
);

expect(component).not.toContainReactText(`€${money.amount}`);
expect(component).not.toContainReactText(`${money.amount}`);
expect(component).toContainReactText(`19`);
});
});
44 changes: 43 additions & 1 deletion packages/hydrogen/src/hooks/useMoney/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export type UseMoneyValue = {
* The `MoneyV2` object provided as an argument to the hook.
*/
original: MoneyV2;
/**
* A string with trailing zeros removed from the fractional part, if any exist. If there are no trailing zeros, then the fractional part remains.
* For example, `$640.00` turns into `$640`.
* `$640.42` remains `$640.42`.
*/
withoutTrailingZeros: string;
/**
* A string without currency and without trailing zeros removed from the fractional part, if any exist. If there are no trailing zeros, then the fractional part remains.
* For example, `$640.00` turns into `640`.
* `$640.42` turns into `640.42`.
*/
withoutTrailingZerosAndCurrency: string;
};

/**
Expand All @@ -56,8 +68,10 @@ export function useMoney(money: MoneyV2): UseMoneyValue {

const amount = parseFloat(money.amount);

const standardCurrencyFormatter = new Intl.NumberFormat(locale, options);

const value = useMemo(
() => new Intl.NumberFormat(locale, options).format(amount),
() => standardCurrencyFormatter.format(amount),
[amount, locale, options]
);

Expand All @@ -73,6 +87,32 @@ export function useMoney(money: MoneyV2): UseMoneyValue {
currencyDisplay: 'narrowSymbol',
}).formatToParts(amount);

const withoutTrailingZerosFormatter = new Intl.NumberFormat(locale, {
...options,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});

const withoutCurrencyFormatter = new Intl.NumberFormat(locale);

const withoutTrailingZerosOrCurrencyFormatter = new Intl.NumberFormat(
locale,
{
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}
);

const withoutTrailingZeros =
amount % 1 === 0
? withoutTrailingZerosFormatter.format(amount)
: standardCurrencyFormatter.format(amount);

const withoutTrailingZerosAndCurrency =
amount % 1 === 0
? withoutTrailingZerosOrCurrencyFormatter.format(amount)
: withoutCurrencyFormatter.format(amount);

const moneyValue = useMemo<UseMoneyValue>(
() => ({
currencyCode: money.currencyCode,
Expand All @@ -96,6 +136,8 @@ export function useMoney(money: MoneyV2): UseMoneyValue {
.map((part) => part.value)
.join(''),
original: money,
withoutTrailingZeros,
withoutTrailingZerosAndCurrency,
}),
[baseParts, money, nameParts, narrowParts, value]
);
Expand Down
72 changes: 52 additions & 20 deletions packages/hydrogen/src/hooks/useMoney/tests/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,60 @@ import {useMoney} from '../hooks';

const mountUseMoney = createMountableHook(useMoney);

it('returns an object with all of the details about the money', async () => {
const money = await mountUseMoney({
amount: '19.99',
currencyCode: CurrencyCode.Usd,
});
describe(`useMoney`, () => {
it('returns an object with all of the details about the money', async () => {
const money = await mountUseMoney({
amount: '19.99',
currencyCode: CurrencyCode.Usd,
});

expect(money).toEqual({
amount: '19.99',
currencyCode: 'USD',
currencyName: 'US dollars',
currencyNarrowSymbol: '$',
currencySymbol: '$',
localizedString: '$19.99',
original: {
expect(money).toEqual({
amount: '19.99',
currencyCode: 'USD',
currencyName: 'US dollars',
currencyNarrowSymbol: '$',
currencySymbol: '$',
localizedString: '$19.99',
original: {
amount: '19.99',
currencyCode: CurrencyCode.Usd,
},
parts: [
{type: 'currency', value: '$'},
{type: 'integer', value: '19'},
{type: 'decimal', value: '.'},
{type: 'fraction', value: '99'},
],
withoutTrailingZeros: '$19.99',
withoutTrailingZerosAndCurrency: '19.99',
});
});

it(`removes trailing zeros when necessary`, async () => {
const money = await mountUseMoney({
amount: '19.00',
currencyCode: CurrencyCode.Usd,
},
parts: [
{type: 'currency', value: '$'},
{type: 'integer', value: '19'},
{type: 'decimal', value: '.'},
{type: 'fraction', value: '99'},
],
});

expect(money).toEqual({
amount: '19.00',
currencyCode: 'USD',
currencyName: 'US dollars',
currencyNarrowSymbol: '$',
currencySymbol: '$',
localizedString: '$19.00',
original: {
amount: '19.00',
currencyCode: CurrencyCode.Usd,
},
parts: [
{type: 'currency', value: '$'},
{type: 'integer', value: '19'},
{type: 'decimal', value: '.'},
{type: 'fraction', value: '00'},
],
withoutTrailingZeros: '$19',
withoutTrailingZerosAndCurrency: '19',
});
});
});