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
30 changes: 30 additions & 0 deletions .changeset/delivery-address-replace-mutation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@shopify/hydrogen': minor
---

Add `cartDeliveryAddressesReplaceDefault` to handle the new `cartDeliveryAddressesReplace` Storefront API mutation (2025-10)

This new mutation replaces all delivery addresses on a cart in a single operation.

**Usage via cart handler:**
```typescript
const result = await cart.replaceDeliveryAddresses([
{
address: {
deliveryAddress: {
address1: '123 Main St',
city: 'Anytown',
countryCode: 'US'
}
},
selected: true
}
]);
```

**Usage via CartForm:**
```tsx
<CartForm action={CartForm.ACTIONS.DeliveryAddressesReplace}>
{/* form inputs */}
</CartForm>
```
1 change: 1 addition & 0 deletions packages/hydrogen/src/cart/CartForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ describe('<CartForm />', () => {
DeliveryAddressesAdd: 'DeliveryAddressesAdd',
DeliveryAddressesUpdate: 'DeliveryAddressesUpdate',
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
DeliveryAddressesReplace: 'DeliveryAddressesReplace',
DiscountCodesUpdate: 'DiscountCodesUpdate',
GiftCardCodesUpdate: 'GiftCardCodesUpdate',
GiftCardCodesRemove: 'GiftCardCodesRemove',
Comment on lines 85 to 91
Copy link
Contributor

Choose a reason for hiding this comment

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

[Re: lines +80 to +100]

this is a very weird test, it doesn’t verify anything, only makes it so we have to copy paste this twice

i’d recommend doing the satisfies thing i did in the other PR

correct me if im wrong but this test is asserting "all the cart actions are present in the CartForm.ACTIONS object"

See this comment inline on Graphite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree. Since we have plans to overhaul all these tests after the release we can get to it then

Expand Down
17 changes: 17 additions & 0 deletions packages/hydrogen/src/cart/CartForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ type CartDeliveryAddressesUpdateRequire = {
} & OtherFormData;
};

type CartDeliveryAddressesReplaceProps = {
action: 'DeliveryAddressesReplace';
inputs?: {
addresses: Array<CartSelectableAddressInput>;
} & OtherFormData;
};

type CartDeliveryAddressesReplaceRequire = {
action: 'DeliveryAddressesReplace';
inputs: {
addresses: Array<CartSelectableAddressInput>;
} & OtherFormData;
};

type CartCustomProps = {
action: `Custom${string}`;
inputs?: Record<string, unknown>;
Expand Down Expand Up @@ -289,6 +303,7 @@ type CartActionInputProps =
| CartDeliveryAddressesAddProps
| CartDeliveryAddressesRemoveProps
| CartDeliveryAddressesUpdateProps
| CartDeliveryAddressesReplaceProps
| CartCustomProps;

export type CartActionInput =
Expand All @@ -308,6 +323,7 @@ export type CartActionInput =
| CartDeliveryAddressesAddRequire
| CartDeliveryAddressesRemoveRequire
| CartDeliveryAddressesUpdateRequire
| CartDeliveryAddressesReplaceRequire
| CartCustomRequire;

type CartFormProps = CartActionInputProps & CartFormCommonProps;
Expand Down Expand Up @@ -356,6 +372,7 @@ CartForm.ACTIONS = {
DeliveryAddressesAdd: 'DeliveryAddressesAdd',
DeliveryAddressesUpdate: 'DeliveryAddressesUpdate',
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
DeliveryAddressesReplace: 'DeliveryAddressesReplace',
} as const;

function getFormInput(formData: FormData): CartActionInput {
Expand Down
7 changes: 4 additions & 3 deletions packages/hydrogen/src/cart/createCartHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('createCartHandler', () => {
const cart = getCartHandler();

expectTypeOf(cart).toEqualTypeOf<HydrogenCart>;
expect(Object.keys(cart)).toHaveLength(19);
expect(Object.keys(cart)).toHaveLength(20);
expect(cart).toHaveProperty('get');
expect(cart).toHaveProperty('getCartId');
expect(cart).toHaveProperty('setCartId');
Expand All @@ -57,6 +57,7 @@ describe('createCartHandler', () => {
expect(cart).toHaveProperty('addDeliveryAddresses');
expect(cart).toHaveProperty('removeDeliveryAddresses');
expect(cart).toHaveProperty('updateDeliveryAddresses');
expect(cart).toHaveProperty('replaceDeliveryAddresses');
});

it('can add custom methods', () => {
Expand All @@ -72,7 +73,7 @@ describe('createCartHandler', () => {
});

expectTypeOf(cart).toEqualTypeOf<HydrogenCartCustom<{foo: () => 'bar'}>>;
expect(Object.keys(cart)).toHaveLength(20);
expect(Object.keys(cart)).toHaveLength(21);
expect(cart.foo()).toBe('bar');
});

Expand All @@ -86,7 +87,7 @@ describe('createCartHandler', () => {
});

expectTypeOf(cart).toEqualTypeOf<HydrogenCart>;
expect(Object.keys(cart)).toHaveLength(19);
expect(Object.keys(cart)).toHaveLength(20);
expect(await cart.get()).toBe('bar');
});

Expand Down
36 changes: 36 additions & 0 deletions packages/hydrogen/src/cart/createCartHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ import {
type CartDeliveryAddressesUpdateFunction,
cartDeliveryAddressesUpdateDefault,
} from './queries/cartDeliveryAddressesUpdateDefault';
import {
type CartDeliveryAddressesReplaceFunction,
cartDeliveryAddressesReplaceDefault,
} from './queries/cartDeliveryAddressesReplaceDefault';
import type {CartBuyerIdentityInput} from '@shopify/hydrogen-react/storefront-api-types';

export type CartHandlerOptions = {
Expand Down Expand Up @@ -180,6 +184,32 @@ export type HydrogenCart = {
updateDeliveryAddresses: ReturnType<
typeof cartDeliveryAddressesUpdateDefault
>;
/**
* Replaces all delivery addresses on the cart.
*
* This function sends a mutation to the storefront API to replace all delivery addresses on the cart
* with the provided addresses. It returns the result of the mutation, including any errors that occurred.
*
* @param {CartQueryOptions} options - The options for the cart query, including the storefront API client and cart fragment.
* @returns {CartDeliveryAddressesReplaceFunction} - A function that takes an array of addresses and optional parameters, and returns the result of the API call.
*
* @example
* const result = await cart.replaceDeliveryAddresses([
* {
* address: {
* deliveryAddress: {
* address1: '123 Main St',
* city: 'Anytown',
* countryCode: 'US'
* }
* },
* selected: true
* }
* ], { someOptionalParam: 'value' });
*/
replaceDeliveryAddresses: ReturnType<
typeof cartDeliveryAddressesReplaceDefault
>;
};

export type HydrogenCartCustom<
Expand Down Expand Up @@ -310,6 +340,8 @@ export function createCartHandler<TCustomMethods extends CustomMethodsBase>(
addDeliveryAddresses: cartDeliveryAddressesAddDefault(mutateOptions),
removeDeliveryAddresses: cartDeliveryAddressesRemoveDefault(mutateOptions),
updateDeliveryAddresses: cartDeliveryAddressesUpdateDefault(mutateOptions),
replaceDeliveryAddresses:
cartDeliveryAddressesReplaceDefault(mutateOptions),
};

if ('customMethods' in options) {
Expand Down Expand Up @@ -408,6 +440,10 @@ export type HydrogenCartForDocs = {
* Update cart delivery addresses.
*/
updateDeliveryAddresses?: CartDeliveryAddressesUpdateFunction;
/**
* Replace all delivery addresses on the cart.
*/
replaceDeliveryAddresses?: CartDeliveryAddressesReplaceFunction;
/**
* Updates additional information (attributes) in the cart.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'cartDeliveryAddressesReplace',
category: 'utilities',
subCategory: 'cart',
isVisualComponent: false,
related: [],
description:
'Creates a function that accepts an array of [CartSelectableAddressInput](/docs/api/storefront/2025-10/input-objects/CartSelectableAddressInput) to replace all delivery addresses on a cart',
type: 'utility',
defaultExample: {
description:
'Replace all delivery addresses on a cart with a new set of addresses',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './cartDeliveryAddressesReplaceDefault.example.js',
language: 'js',
},
],
title: 'example',
},
},
definitions: [
{
title: 'cartDeliveryAddressesReplaceDefault',
type: 'CartDeliveryAddressesReplaceDefaultGeneratedType',
description: '',
},
],
};

export default data;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {cartDeliveryAddressesReplaceDefault} from '@shopify/hydrogen';

const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
storefront,
getCartId,
});

const result = await replaceDeliveryAddresses(
[
{
address: {
deliveryAddress: {
address1: '<your-address1>',
address2: '<your-address2>',
city: '<your-city>',
company: '<your-company>',
countryCode: 'AC',
firstName: '<your-firstName>',
lastName: '<your-lastName>',
phone: '<your-phone>',
provinceCode: '<your-provinceCode>',
zip: '<your-zip>',
},
},
selected: true,
},
],
{someOptionalParam: 'value'},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {describe, it, expect} from 'vitest';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: these tests are bad. there's existing precedent, we're going to fix these after 2025-10 release

Copy link
Contributor

Choose a reason for hiding this comment

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

the ones we don’t fix should be removed, because this is leading LLMs to generate more dud tests like these when prompted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed! (for the future, once we overhaul the tests). I would love for this codebase to be a perfect example of fabulous tests!

import {CART_ID, mockCreateStorefrontClient} from '../cart-test-helper';
import {cartDeliveryAddressesReplaceDefault} from './cartDeliveryAddressesReplaceDefault';

describe('cartDeliveryAddressesReplaceDefault', () => {
it('should return a default cart delivery address replace implementation', async () => {
const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
storefront: mockCreateStorefrontClient(),
getCartId: () => CART_ID,
});

const result = await replaceDeliveryAddresses([]);

expect(result.cart).toHaveProperty('id', CART_ID);
});

it('can override cartFragment', async () => {
const cartFragment = 'cartFragmentOverride';
const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
storefront: mockCreateStorefrontClient(),
getCartId: () => CART_ID,
cartFragment,
});

const result = await replaceDeliveryAddresses([]);

expect(result.cart).toHaveProperty('id', CART_ID);
expect(result.userErrors?.[0]).toContain(cartFragment);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {StorefrontApiErrors, formatAPIResult} from '../../storefront';
import type {CartSelectableAddressInput} from '@shopify/hydrogen-react/storefront-api-types';
import {
CART_WARNING_FRAGMENT,
MINIMAL_CART_FRAGMENT,
USER_ERROR_FRAGMENT,
} from './cart-fragments';
import type {
CartOptionalInput,
CartQueryData,
CartQueryDataReturn,
CartQueryOptions,
} from './cart-types';

export type CartDeliveryAddressesReplaceFunction = (
addresses: Array<CartSelectableAddressInput>,
optionalParams?: CartOptionalInput,
) => Promise<CartQueryDataReturn>;

/**
* Replaces all delivery addresses on the cart.
*
* This function sends a mutation to the storefront API to replace all delivery addresses on the cart
* with the provided addresses. It returns the result of the mutation, including any errors that occurred.
*
* @param {CartQueryOptions} options - The options for the cart query, including the storefront API client and cart fragment.
* @returns {CartDeliveryAddressesReplaceFunction} - A function that takes an array of addresses and optional parameters, and returns the result of the API call.
*
* @example
* const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({ storefront, getCartId });
* const result = await replaceDeliveryAddresses([
* {
* address: {
* deliveryAddress: {
* address1: '123 Main St',
* city: 'Anytown',
* countryCode: 'US'
* }
* },
* selected: true
* }
* ], { someOptionalParam: 'value' }
* );
*/
export function cartDeliveryAddressesReplaceDefault(
options: CartQueryOptions,
): CartDeliveryAddressesReplaceFunction {
return async (
addresses: Array<CartSelectableAddressInput>,
optionalParams,
) => {
const {cartDeliveryAddressesReplace, errors} =
await options.storefront.mutate<{
cartDeliveryAddressesReplace: CartQueryData;
errors: StorefrontApiErrors;
}>(CART_DELIVERY_ADDRESSES_REPLACE_MUTATION(options.cartFragment), {
variables: {
cartId: options.getCartId(),
addresses,
...optionalParams,
},
});

return formatAPIResult(cartDeliveryAddressesReplace, errors);
};
}

//! @see: https://shopify.dev/docs/api/storefront/2025-10/mutations/cartDeliveryAddressesReplace
export const CART_DELIVERY_ADDRESSES_REPLACE_MUTATION = (
cartFragment = MINIMAL_CART_FRAGMENT,
) => `#graphql
mutation cartDeliveryAddressesReplace(
$cartId: ID!
$addresses: [CartSelectableAddressInput!]!,
$country: CountryCode = ZZ
$language: LanguageCode
) @inContext(country: $country, language: $language) {
cartDeliveryAddressesReplace(addresses: $addresses, cartId: $cartId) {
cart {
...CartApiMutation
}
userErrors {
...CartApiError
}
warnings {
...CartApiWarning
}
}
}
${cartFragment}
${USER_ERROR_FRAGMENT}
${CART_WARNING_FRAGMENT}
`;
Loading