diff --git a/.changeset/delivery-address-replace-mutation.md b/.changeset/delivery-address-replace-mutation.md new file mode 100644 index 0000000000..2736ca6c06 --- /dev/null +++ b/.changeset/delivery-address-replace-mutation.md @@ -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 + + {/* form inputs */} + +``` diff --git a/packages/hydrogen/src/cart/CartForm.test.tsx b/packages/hydrogen/src/cart/CartForm.test.tsx index e581ce0114..31eb622b37 100644 --- a/packages/hydrogen/src/cart/CartForm.test.tsx +++ b/packages/hydrogen/src/cart/CartForm.test.tsx @@ -85,6 +85,7 @@ describe('', () => { DeliveryAddressesAdd: 'DeliveryAddressesAdd', DeliveryAddressesUpdate: 'DeliveryAddressesUpdate', DeliveryAddressesRemove: 'DeliveryAddressesRemove', + DeliveryAddressesReplace: 'DeliveryAddressesReplace', DiscountCodesUpdate: 'DiscountCodesUpdate', GiftCardCodesUpdate: 'GiftCardCodesUpdate', GiftCardCodesRemove: 'GiftCardCodesRemove', diff --git a/packages/hydrogen/src/cart/CartForm.tsx b/packages/hydrogen/src/cart/CartForm.tsx index 5e5d92c985..e083031075 100644 --- a/packages/hydrogen/src/cart/CartForm.tsx +++ b/packages/hydrogen/src/cart/CartForm.tsx @@ -245,6 +245,20 @@ type CartDeliveryAddressesUpdateRequire = { } & OtherFormData; }; +type CartDeliveryAddressesReplaceProps = { + action: 'DeliveryAddressesReplace'; + inputs?: { + addresses: Array; + } & OtherFormData; +}; + +type CartDeliveryAddressesReplaceRequire = { + action: 'DeliveryAddressesReplace'; + inputs: { + addresses: Array; + } & OtherFormData; +}; + type CartCustomProps = { action: `Custom${string}`; inputs?: Record; @@ -289,6 +303,7 @@ type CartActionInputProps = | CartDeliveryAddressesAddProps | CartDeliveryAddressesRemoveProps | CartDeliveryAddressesUpdateProps + | CartDeliveryAddressesReplaceProps | CartCustomProps; export type CartActionInput = @@ -308,6 +323,7 @@ export type CartActionInput = | CartDeliveryAddressesAddRequire | CartDeliveryAddressesRemoveRequire | CartDeliveryAddressesUpdateRequire + | CartDeliveryAddressesReplaceRequire | CartCustomRequire; type CartFormProps = CartActionInputProps & CartFormCommonProps; @@ -356,6 +372,7 @@ CartForm.ACTIONS = { DeliveryAddressesAdd: 'DeliveryAddressesAdd', DeliveryAddressesUpdate: 'DeliveryAddressesUpdate', DeliveryAddressesRemove: 'DeliveryAddressesRemove', + DeliveryAddressesReplace: 'DeliveryAddressesReplace', } as const; function getFormInput(formData: FormData): CartActionInput { diff --git a/packages/hydrogen/src/cart/createCartHandler.test.ts b/packages/hydrogen/src/cart/createCartHandler.test.ts index 1d3a0318bb..3013c9fc9b 100644 --- a/packages/hydrogen/src/cart/createCartHandler.test.ts +++ b/packages/hydrogen/src/cart/createCartHandler.test.ts @@ -37,7 +37,7 @@ describe('createCartHandler', () => { const cart = getCartHandler(); expectTypeOf(cart).toEqualTypeOf; - expect(Object.keys(cart)).toHaveLength(19); + expect(Object.keys(cart)).toHaveLength(20); expect(cart).toHaveProperty('get'); expect(cart).toHaveProperty('getCartId'); expect(cart).toHaveProperty('setCartId'); @@ -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', () => { @@ -72,7 +73,7 @@ describe('createCartHandler', () => { }); expectTypeOf(cart).toEqualTypeOf 'bar'}>>; - expect(Object.keys(cart)).toHaveLength(20); + expect(Object.keys(cart)).toHaveLength(21); expect(cart.foo()).toBe('bar'); }); @@ -86,7 +87,7 @@ describe('createCartHandler', () => { }); expectTypeOf(cart).toEqualTypeOf; - expect(Object.keys(cart)).toHaveLength(19); + expect(Object.keys(cart)).toHaveLength(20); expect(await cart.get()).toBe('bar'); }); diff --git a/packages/hydrogen/src/cart/createCartHandler.ts b/packages/hydrogen/src/cart/createCartHandler.ts index b3a3954765..c0c46003e3 100644 --- a/packages/hydrogen/src/cart/createCartHandler.ts +++ b/packages/hydrogen/src/cart/createCartHandler.ts @@ -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 = { @@ -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< @@ -310,6 +340,8 @@ export function createCartHandler( addDeliveryAddresses: cartDeliveryAddressesAddDefault(mutateOptions), removeDeliveryAddresses: cartDeliveryAddressesRemoveDefault(mutateOptions), updateDeliveryAddresses: cartDeliveryAddressesUpdateDefault(mutateOptions), + replaceDeliveryAddresses: + cartDeliveryAddressesReplaceDefault(mutateOptions), }; if ('customMethods' in options) { @@ -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. */ diff --git a/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.doc.ts b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.doc.ts new file mode 100644 index 0000000000..c4d6ddf65e --- /dev/null +++ b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.doc.ts @@ -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; diff --git a/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.example.js b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.example.js new file mode 100644 index 0000000000..0b9fa4fa31 --- /dev/null +++ b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.example.js @@ -0,0 +1,29 @@ +import {cartDeliveryAddressesReplaceDefault} from '@shopify/hydrogen'; + +const replaceDeliveryAddresses = cartDeliveryAddressesReplaceDefault({ + storefront, + getCartId, +}); + +const result = await replaceDeliveryAddresses( + [ + { + address: { + deliveryAddress: { + address1: '', + address2: '', + city: '', + company: '', + countryCode: 'AC', + firstName: '', + lastName: '', + phone: '', + provinceCode: '', + zip: '', + }, + }, + selected: true, + }, + ], + {someOptionalParam: 'value'}, +); diff --git a/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.test.ts b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.test.ts new file mode 100644 index 0000000000..caf28daa1f --- /dev/null +++ b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.test.ts @@ -0,0 +1,30 @@ +import {describe, it, expect} from 'vitest'; +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); + }); +}); diff --git a/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.tsx b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.tsx new file mode 100644 index 0000000000..e2d6f43086 --- /dev/null +++ b/packages/hydrogen/src/cart/queries/cartDeliveryAddressesReplaceDefault.tsx @@ -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, + optionalParams?: CartOptionalInput, +) => Promise; + +/** + * 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, + 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} +`;