Skip to content

Commit ad18687

Browse files
committed
feat(cart): add cartDeliveryAddressesReplace mutation support
Enable batch replacement of all cart delivery addresses via new Storefront API 2025-10 mutation. Why: The existing Add/Remove/Update mutations require multiple calls to fully replace addresses. The new Replace mutation simplifies workflows where the entire address list should be swapped atomically. How: - Add cartDeliveryAddressesReplaceDefault.tsx following sibling patterns - Integrate replaceDeliveryAddresses method in createCartHandler - Add DeliveryAddressesReplace action to CartForm.ACTIONS - Include changeset for minor version bump Usage: const result = await cart.replaceDeliveryAddresses([ { address: { deliveryAddress: {...} }, selected: true } ]);
1 parent bce6ffc commit ad18687

9 files changed

+274
-3
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@shopify/hydrogen': minor
3+
---
4+
5+
Add `cartDeliveryAddressesReplaceDefault` to handle the new `cartDeliveryAddressesReplace` Storefront API mutation (2025-10)
6+
7+
This new mutation replaces all delivery addresses on a cart in a single operation.
8+
9+
**Usage via cart handler:**
10+
```typescript
11+
const result = await cart.replaceDeliveryAddresses([
12+
{
13+
address: {
14+
deliveryAddress: {
15+
address1: '123 Main St',
16+
city: 'Anytown',
17+
countryCode: 'US'
18+
}
19+
},
20+
selected: true
21+
}
22+
]);
23+
```
24+
25+
**Usage via CartForm:**
26+
```tsx
27+
<CartForm action={CartForm.ACTIONS.DeliveryAddressesReplace}>
28+
{/* form inputs */}
29+
</CartForm>
30+
```

packages/hydrogen/src/cart/CartForm.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('<CartForm />', () => {
8585
DeliveryAddressesAdd: 'DeliveryAddressesAdd',
8686
DeliveryAddressesUpdate: 'DeliveryAddressesUpdate',
8787
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
88+
DeliveryAddressesReplace: "DeliveryAddressesReplace",
8889
DiscountCodesUpdate: 'DiscountCodesUpdate',
8990
GiftCardCodesUpdate: 'GiftCardCodesUpdate',
9091
GiftCardCodesRemove: 'GiftCardCodesRemove',

packages/hydrogen/src/cart/CartForm.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ type CartDeliveryAddressesUpdateRequire = {
245245
} & OtherFormData;
246246
};
247247

248+
type CartDeliveryAddressesReplaceProps = {
249+
action: 'DeliveryAddressesReplace';
250+
inputs?: {
251+
addresses: Array<CartSelectableAddressInput>;
252+
} & OtherFormData;
253+
};
254+
255+
type CartDeliveryAddressesReplaceRequire = {
256+
action: 'DeliveryAddressesReplace';
257+
inputs: {
258+
addresses: Array<CartSelectableAddressInput>;
259+
} & OtherFormData;
260+
};
261+
248262
type CartCustomProps = {
249263
action: `Custom${string}`;
250264
inputs?: Record<string, unknown>;
@@ -289,6 +303,7 @@ type CartActionInputProps =
289303
| CartDeliveryAddressesAddProps
290304
| CartDeliveryAddressesRemoveProps
291305
| CartDeliveryAddressesUpdateProps
306+
| CartDeliveryAddressesReplaceProps
292307
| CartCustomProps;
293308

294309
export type CartActionInput =
@@ -308,6 +323,7 @@ export type CartActionInput =
308323
| CartDeliveryAddressesAddRequire
309324
| CartDeliveryAddressesRemoveRequire
310325
| CartDeliveryAddressesUpdateRequire
326+
| CartDeliveryAddressesReplaceRequire
311327
| CartCustomRequire;
312328

313329
type CartFormProps = CartActionInputProps & CartFormCommonProps;
@@ -356,6 +372,7 @@ CartForm.ACTIONS = {
356372
DeliveryAddressesAdd: 'DeliveryAddressesAdd',
357373
DeliveryAddressesUpdate: 'DeliveryAddressesUpdate',
358374
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
375+
DeliveryAddressesReplace: 'DeliveryAddressesReplace',
359376
} as const;
360377

361378
function getFormInput(formData: FormData): CartActionInput {

packages/hydrogen/src/cart/createCartHandler.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('createCartHandler', () => {
3737
const cart = getCartHandler();
3838

3939
expectTypeOf(cart).toEqualTypeOf<HydrogenCart>;
40-
expect(Object.keys(cart)).toHaveLength(19);
40+
expect(Object.keys(cart)).toHaveLength(20);
4141
expect(cart).toHaveProperty('get');
4242
expect(cart).toHaveProperty('getCartId');
4343
expect(cart).toHaveProperty('setCartId');
@@ -57,6 +57,7 @@ describe('createCartHandler', () => {
5757
expect(cart).toHaveProperty('addDeliveryAddresses');
5858
expect(cart).toHaveProperty('removeDeliveryAddresses');
5959
expect(cart).toHaveProperty('updateDeliveryAddresses');
60+
expect(cart).toHaveProperty('replaceDeliveryAddresses');
6061
});
6162

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

7475
expectTypeOf(cart).toEqualTypeOf<HydrogenCartCustom<{foo: () => 'bar'}>>;
75-
expect(Object.keys(cart)).toHaveLength(20);
76+
expect(Object.keys(cart)).toHaveLength(21);
7677
expect(cart.foo()).toBe('bar');
7778
});
7879

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

8889
expectTypeOf(cart).toEqualTypeOf<HydrogenCart>;
89-
expect(Object.keys(cart)).toHaveLength(19);
90+
expect(Object.keys(cart)).toHaveLength(20);
9091
expect(await cart.get()).toBe('bar');
9192
});
9293

packages/hydrogen/src/cart/createCartHandler.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ import {
6565
type CartDeliveryAddressesUpdateFunction,
6666
cartDeliveryAddressesUpdateDefault,
6767
} from './queries/cartDeliveryAddressesUpdateDefault';
68+
import {
69+
type CartDeliveryAddressesReplaceFunction,
70+
cartDeliveryAddressesReplaceDefault,
71+
} from './queries/cartDeliveryAddressesReplaceDefault';
6872
import type {CartBuyerIdentityInput} from '@shopify/hydrogen-react/storefront-api-types';
6973

7074
export type CartHandlerOptions = {
@@ -180,6 +184,32 @@ export type HydrogenCart = {
180184
updateDeliveryAddresses: ReturnType<
181185
typeof cartDeliveryAddressesUpdateDefault
182186
>;
187+
/**
188+
* Replaces all delivery addresses on the cart.
189+
*
190+
* This function sends a mutation to the storefront API to replace all delivery addresses on the cart
191+
* with the provided addresses. It returns the result of the mutation, including any errors that occurred.
192+
*
193+
* @param {CartQueryOptions} options - The options for the cart query, including the storefront API client and cart fragment.
194+
* @returns {CartDeliveryAddressesReplaceFunction} - A function that takes an array of addresses and optional parameters, and returns the result of the API call.
195+
*
196+
* @example
197+
* const result = await cart.replaceDeliveryAddresses([
198+
* {
199+
* address: {
200+
* deliveryAddress: {
201+
* address1: '123 Main St',
202+
* city: 'Anytown',
203+
* countryCode: 'US'
204+
* }
205+
* },
206+
* selected: true
207+
* }
208+
* ], { someOptionalParam: 'value' });
209+
*/
210+
replaceDeliveryAddresses: ReturnType<
211+
typeof cartDeliveryAddressesReplaceDefault
212+
>;
183213
};
184214

185215
export type HydrogenCartCustom<
@@ -310,6 +340,8 @@ export function createCartHandler<TCustomMethods extends CustomMethodsBase>(
310340
addDeliveryAddresses: cartDeliveryAddressesAddDefault(mutateOptions),
311341
removeDeliveryAddresses: cartDeliveryAddressesRemoveDefault(mutateOptions),
312342
updateDeliveryAddresses: cartDeliveryAddressesUpdateDefault(mutateOptions),
343+
replaceDeliveryAddresses:
344+
cartDeliveryAddressesReplaceDefault(mutateOptions),
313345
};
314346

315347
if ('customMethods' in options) {
@@ -408,6 +440,10 @@ export type HydrogenCartForDocs = {
408440
* Update cart delivery addresses.
409441
*/
410442
updateDeliveryAddresses?: CartDeliveryAddressesUpdateFunction;
443+
/**
444+
* Replace all delivery addresses on the cart.
445+
*/
446+
replaceDeliveryAddresses?: CartDeliveryAddressesReplaceFunction;
411447
/**
412448
* Updates additional information (attributes) in the cart.
413449
*/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
2+
3+
const data: ReferenceEntityTemplateSchema = {
4+
name: 'cartDeliveryAddressesReplace',
5+
category: 'utilities',
6+
subCategory: 'cart',
7+
isVisualComponent: false,
8+
related: [],
9+
description:
10+
'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',
11+
type: 'utility',
12+
defaultExample: {
13+
description: 'This is the default example',
14+
codeblock: {
15+
tabs: [
16+
{
17+
title: 'JavaScript',
18+
code: './cartDeliveryAddressesReplaceDefault.example.js',
19+
language: 'js',
20+
},
21+
],
22+
title: 'example',
23+
},
24+
},
25+
definitions: [
26+
{
27+
title: 'cartDeliveryAddressesReplaceDefault',
28+
type: 'CartDeliveryAddressesReplaceDefaultGeneratedType',
29+
description: '',
30+
},
31+
],
32+
};
33+
34+
export default data;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {cartDeliveryAddressesReplaceDefault} from '@shopify/hydrogen';
2+
3+
const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
4+
storefront,
5+
getCartId,
6+
});
7+
8+
const result = await replaceDeliveryAddresses(
9+
[
10+
{
11+
address: {
12+
deliveryAddress: {
13+
address1: '<your-address1>',
14+
address2: '<your-address2>',
15+
city: '<your-city>',
16+
company: '<your-company>',
17+
countryCode: 'AC',
18+
firstName: '<your-firstName>',
19+
lastName: '<your-lastName>',
20+
phone: '<your-phone>',
21+
provinceCode: '<your-provinceCode>',
22+
zip: '<your-zip>',
23+
},
24+
},
25+
selected: true,
26+
},
27+
],
28+
{someOptionalParam: 'value'},
29+
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {describe, it, expect} from 'vitest';
2+
import {CART_ID, mockCreateStorefrontClient} from '../cart-test-helper';
3+
import {cartDeliveryAddressesReplaceDefault} from './cartDeliveryAddressesReplaceDefault';
4+
5+
describe('cartDeliveryAddressesReplaceDefault', () => {
6+
it('should return a default cart delivery address replace implementation', async () => {
7+
const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
8+
storefront: mockCreateStorefrontClient(),
9+
getCartId: () => CART_ID,
10+
});
11+
12+
const result = await replaceDeliveryAddresses([]);
13+
14+
expect(result.cart).toHaveProperty('id', CART_ID);
15+
});
16+
17+
it('can override cartFragment', async () => {
18+
const cartFragment = 'cartFragmentOverride';
19+
const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({
20+
storefront: mockCreateStorefrontClient(),
21+
getCartId: () => CART_ID,
22+
cartFragment,
23+
});
24+
25+
const result = await replaceDeliveryAddresses([]);
26+
27+
expect(result.cart).toHaveProperty('id', CART_ID);
28+
expect(result.userErrors?.[0]).toContain(cartFragment);
29+
});
30+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {StorefrontApiErrors, formatAPIResult} from '../../storefront';
2+
import type {CartSelectableAddressInput} from '@shopify/hydrogen-react/storefront-api-types';
3+
import {
4+
CART_WARNING_FRAGMENT,
5+
MINIMAL_CART_FRAGMENT,
6+
USER_ERROR_FRAGMENT,
7+
} from './cart-fragments';
8+
import type {
9+
CartOptionalInput,
10+
CartQueryData,
11+
CartQueryDataReturn,
12+
CartQueryOptions,
13+
} from './cart-types';
14+
15+
export type CartDeliveryAddressesReplaceFunction = (
16+
addresses: Array<CartSelectableAddressInput>,
17+
optionalParams?: CartOptionalInput,
18+
) => Promise<CartQueryDataReturn>;
19+
20+
/**
21+
* Replaces all delivery addresses on the cart.
22+
*
23+
* This function sends a mutation to the storefront API to replace all delivery addresses on the cart
24+
* with the provided addresses. It returns the result of the mutation, including any errors that occurred.
25+
*
26+
* @param {CartQueryOptions} options - The options for the cart query, including the storefront API client and cart fragment.
27+
* @returns {CartDeliveryAddressesReplaceFunction} - A function that takes an array of addresses and optional parameters, and returns the result of the API call.
28+
*
29+
* @example
30+
* const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({ storefront, getCartId });
31+
* const result = await replaceDeliveryAddresses([
32+
* {
33+
* address: {
34+
* deliveryAddress: {
35+
* address1: '123 Main St',
36+
* city: 'Anytown',
37+
* countryCode: 'US'
38+
* }
39+
* },
40+
* selected: true
41+
* }
42+
* ], { someOptionalParam: 'value' }
43+
* );
44+
*/
45+
export function cartDeliveryAddressesReplaceDefault(
46+
options: CartQueryOptions,
47+
): CartDeliveryAddressesReplaceFunction {
48+
return async (
49+
addresses: Array<CartSelectableAddressInput>,
50+
optionalParams,
51+
) => {
52+
const {cartDeliveryAddressesReplace, errors} =
53+
await options.storefront.mutate<{
54+
cartDeliveryAddressesReplace: CartQueryData;
55+
errors: StorefrontApiErrors;
56+
}>(CART_DELIVERY_ADDRESSES_REPLACE_MUTATION(options.cartFragment), {
57+
variables: {
58+
cartId: options.getCartId(),
59+
addresses,
60+
...optionalParams,
61+
},
62+
});
63+
64+
return formatAPIResult(cartDeliveryAddressesReplace, errors);
65+
};
66+
}
67+
68+
//! @see: https://shopify.dev/docs/api/storefront/2025-10/mutations/cartDeliveryAddressesReplace
69+
export const CART_DELIVERY_ADDRESSES_REPLACE_MUTATION = (
70+
cartFragment = MINIMAL_CART_FRAGMENT,
71+
) => `#graphql
72+
mutation cartDeliveryAddressesReplace(
73+
$cartId: ID!
74+
$addresses: [CartSelectableAddressInput!]!,
75+
$country: CountryCode = ZZ
76+
$language: LanguageCode
77+
) @inContext(country: $country, language: $language) {
78+
cartDeliveryAddressesReplace(addresses: $addresses, cartId: $cartId) {
79+
cart {
80+
...CartApiMutation
81+
}
82+
userErrors {
83+
...CartApiError
84+
}
85+
warnings {
86+
...CartApiWarning
87+
}
88+
}
89+
}
90+
${cartFragment}
91+
${USER_ERROR_FRAGMENT}
92+
${CART_WARNING_FRAGMENT}
93+
`;

0 commit comments

Comments
 (0)