Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .changeset/fix-money-usdc-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@shopify/hydrogen-react': patch
---

Fix Money component compatibility with Customer Account API USDC currency

The 2025-07 API update added USDC currency to Customer Account API but not Storefront API, causing TypeScript errors and runtime failures. This fix:

- Updates Money component to accept MoneyV2 from both Storefront and Customer Account APIs
- Handles unsupported currency codes (like USDC) that Intl.NumberFormat doesn't recognize
- Falls back to decimal formatting with currency code suffix (e.g., "100.00 USDC")
- Maintains 2 decimal places for USDC to reinforce its 1:1 USD peg
73 changes: 47 additions & 26 deletions packages/hydrogen-react/docs/generated/generated_docs_data.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"type": "Generic",
"anchorLink": "authentication",
"title": "Authentication",
"sectionContent": "To use Hydrogen React, you need to authenticate with and make requests to the [Storefront API](/docs/api/storefront). Hydrogen React includes an [API client](/docs/api/hydrogen-react/2025-04/utilities/createstorefrontclient) to securely handle API queries and mutations.\n\nYou can create and manage Storefront API access tokens by installing the [Headless sales channel](https://apps.shopify.com/headless) on your store.\n\nApps have access to [two kinds of tokens](/docs/api/usage/authentication#access-tokens-for-the-storefront-api): a public API token, which can be used in client-side code, and a private API token, which should only be used in server-side contexts and never exposed publicly.",
"sectionContent": "To use Hydrogen React, you need to authenticate with and make requests to the [Storefront API](/docs/api/storefront). Hydrogen React includes an [API client](/docs/api/hydrogen-react/2025-07/utilities/createstorefrontclient) to securely handle API queries and mutations.\n\nYou can create and manage Storefront API access tokens by installing the [Headless sales channel](https://apps.shopify.com/headless) on your store.\n\nApps have access to [two kinds of tokens](/docs/api/usage/authentication#access-tokens-for-the-storefront-api): a public API token, which can be used in client-side code, and a private API token, which should only be used in server-side contexts and never exposed publicly.",
"sectionCard": [
{
"subtitle": "Install",
Expand Down
6 changes: 3 additions & 3 deletions packages/hydrogen-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@
"clean-dist": "rimraf ./dist",
"dev": "run-s dev:demo",
"dev:story": "ladle serve",
"dev:demo": "run-p dev:demo:* copy-storefront-types",
"dev:demo": "run-p dev:demo:* copy-api-types",
"dev:demo:browser-dev": "vite build --watch --emptyOutDir false --clearScreen false --mode devbuild",
"dev:demo:ts": "tsc --watch --emitDeclarationOnly",
"build": "npm-run-all --sequential clean-dist --parallel build:vite:* build:tsc:es --parallel build:tsc:cjs copy-storefront-types",
"build": "npm-run-all --sequential clean-dist --parallel build:vite:* build:tsc:es --parallel build:tsc:cjs copy-api-types",
"build:vite:browser-dev": "vite build --mode devbuild",
"build:vite:browser-prod": "vite build",
"build:vite:node-dev": "vite build --mode devbuild --ssr",
Expand All @@ -119,7 +119,7 @@
"build:vite:umdprod": "vite build --mode umdbuild",
"build:tsc:cjs": "cpy ./dist/types/index.d.ts ./dist/types/ --rename='index.d.cts' --flat",
"build:tsc:es": "tsc --emitDeclarationOnly --project tsconfig.typeoutput.json",
"copy-storefront-types": "cpy ./src/storefront-api-types.d.ts ./dist/types/ --flat",
"copy-api-types": "cpy './src/storefront-api-types.d.ts' './src/customer-account-api-types.d.ts' ./dist/types/ --flat",
"format": "prettier --write \"{src,docs}/**/*\" --ignore-unknown",
"graphql-types": "graphql-codegen --config codegen.ts && npm run format",
"test": "vitest run --coverage",
Expand Down
11 changes: 11 additions & 0 deletions packages/hydrogen-react/src/Money.test.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
UnitPriceMeasurement,
UnitPriceMeasurementMeasuredUnit,
} from './storefront-api-types.js';
import {MoneyV2 as CustomerMoneyV2} from './customer-account-api-types.js';
import {faker} from '@faker-js/faker';

export function getPrice(price: Partial<MoneyV2> = {}): MoneyV2 {
Expand All @@ -12,6 +13,16 @@ export function getPrice(price: Partial<MoneyV2> = {}): MoneyV2 {
};
}

// Helper for Customer Account API MoneyV2 which may have different currency codes
export function getCustomerPrice(
price: Partial<CustomerMoneyV2> = {},
): CustomerMoneyV2 {
return {
currencyCode: price.currencyCode ?? 'USDC', // Use USDC as example of Customer-only currency
amount: price.amount ?? faker.finance.amount(),
};
}

export function getUnitPriceMeasurement(
unitPriceMeasurement: Partial<UnitPriceMeasurement> = {},
): UnitPriceMeasurement {
Expand Down
50 changes: 49 additions & 1 deletion packages/hydrogen-react/src/Money.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {render, screen} from '@testing-library/react';
import {Money} from './Money.js';
import {ShopifyProvider} from './ShopifyProvider.js';
import {getShopifyConfig} from './ShopifyProvider.test.js';
import {getPrice, getUnitPriceMeasurement} from './Money.test.helpers.js';
import {
getPrice,
getCustomerPrice,
getUnitPriceMeasurement,
} from './Money.test.helpers.js';

describe('<Money />', () => {
it('renders a formatted money string', () => {
Expand Down Expand Up @@ -180,4 +184,48 @@ describe('<Money />', () => {
),
).toBeInTheDocument();
});

it('handles Customer Account API MoneyV2 with different currency codes (e.g., USDC)', () => {
// Test that Money component accepts MoneyV2 from Customer Account API
// which may have currency codes not present in Storefront API
const money = getCustomerPrice({
currencyCode: 'USDC',
amount: '100.00',
});

render(<Money data={money} />, {
wrapper: ({children}) => (
<ShopifyProvider {...getShopifyConfig()}>{children}</ShopifyProvider>
),
});

// USDC should render with currency code appended since it's not supported by Intl.NumberFormat
expect(screen.getByText('100.00 USDC')).toBeInTheDocument();
});

it('handles both Storefront and Customer Account MoneyV2 types interchangeably', () => {
// Verify that the component works with both API types
const storefrontMoney = getPrice({currencyCode: 'USD', amount: '50.00'});
const customerMoney = getCustomerPrice({
currencyCode: 'EUR',
amount: '75.00',
});

const {rerender} = render(<Money data={storefrontMoney} />, {
wrapper: ({children}) => (
<ShopifyProvider {...getShopifyConfig()}>{children}</ShopifyProvider>
),
});

expect(screen.getByText('50.00', {exact: false})).toBeInTheDocument();

// Re-render with Customer Account API data
rerender(
<ShopifyProvider {...getShopifyConfig()}>
<Money data={customerMoney} />
</ShopifyProvider>,
);

expect(screen.getByText('75.00', {exact: false})).toBeInTheDocument();
});
});
14 changes: 10 additions & 4 deletions packages/hydrogen-react/src/Money.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {type ReactNode} from 'react';
import {useMoney} from './useMoney.js';
import type {MoneyV2, UnitPriceMeasurement} from './storefront-api-types.js';
import type {MoneyV2 as CustomerMoneyV2} from './customer-account-api-types.js';
import type {PartialDeep} from 'type-fest';

// Support MoneyV2 from both Storefront API and Customer Account API
// The APIs may have different CurrencyCode enums (e.g., Customer Account API added USDC in 2025-07)
// This union type ensures Money component works with data from either API
type AnyMoneyV2 = MoneyV2 | CustomerMoneyV2;

export interface MoneyPropsBase<ComponentGeneric extends React.ElementType> {
/** An HTML tag or React Component to be rendered as the base element wrapper. The default is `div`. */
as?: ComponentGeneric;
/** 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, {recurseIntoArrays: true}>;
/** An object with fields that correspond to the Storefront API's [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2) or Customer Account API's MoneyV2 object. */
data: PartialDeep<AnyMoneyV2, {recurseIntoArrays: true}>;
/** Whether to remove the currency symbol from the output. */
withoutCurrency?: boolean;
/** Whether to remove trailing zeros (fractional money) from the output. */
Expand Down Expand Up @@ -106,8 +112,8 @@ export function Money<ComponentGeneric extends React.ElementType = 'div'>({

// required in order to narrow the money object down and make TS happy
function isMoney(
maybeMoney: PartialDeep<MoneyV2, {recurseIntoArrays: true}>,
): maybeMoney is MoneyV2 {
maybeMoney: PartialDeep<AnyMoneyV2, {recurseIntoArrays: true}>,
): maybeMoney is AnyMoneyV2 {
return (
typeof maybeMoney.amount === 'string' &&
!!maybeMoney.amount &&
Expand Down
48 changes: 48 additions & 0 deletions packages/hydrogen-react/src/useMoney.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,52 @@ describe(`useMoney`, () => {
withoutTrailingZerosAndCurrency: '19',
});
});

it('handles Customer Account API MoneyV2 with USDC currency', () => {
// Test that useMoney works with Customer Account API's MoneyV2
// which may have currency codes not in Storefront API (e.g., USDC)
const customerMoney = {
amount: '100.00',
currencyCode: 'USDC' as const,
};
const {result} = renderHook(() => useMoney(customerMoney));

expect(result.current).toMatchObject({
amount: '100.00',
currencyCode: 'USDC',
currencyName: 'USDC',
currencySymbol: 'USDC',
localizedString: '100.00 USDC',
original: {
amount: '100.00',
currencyCode: 'USDC',
},
withoutTrailingZeros: '100 USDC',
withoutTrailingZerosAndCurrency: '100',
});
});

it('handles both Storefront and Customer Account MoneyV2 types', () => {
// Test with a standard Storefront API currency
const {result: storefrontResult} = renderHook(() =>
useMoney({
amount: '50.00',
currencyCode: 'USD',
}),
);

expect(storefrontResult.current.currencyCode).toBe('USD');
expect(storefrontResult.current.amount).toBe('50.00');

// Test with a Customer Account API specific currency
const {result: customerResult} = renderHook(() =>
useMoney({
amount: '75.00',
currencyCode: 'EUR',
}),
);

expect(customerResult.current.currencyCode).toBe('EUR');
expect(customerResult.current.amount).toBe('75.00');
});
});
Loading
Loading