diff --git a/.changeset/gift-card-add-mutation.md b/.changeset/gift-card-add-mutation.md
new file mode 100644
index 0000000000..9dc0f8fff1
--- /dev/null
+++ b/.changeset/gift-card-add-mutation.md
@@ -0,0 +1,70 @@
+---
+'@shopify/hydrogen': minor
+'@shopify/cli-hydrogen': patch
+'@shopify/create-hydrogen': patch
+---
+
+Add `cartGiftCardCodesAdd` mutation
+
+## New Feature: cartGiftCardCodesAdd
+
+Adds gift card codes without replacing existing ones.
+
+**Before (2025-07):**
+```typescript
+const codes = ['EXISTING1', 'EXISTING2'];
+await cart.updateGiftCardCodes(['EXISTING1', 'EXISTING2', 'NEW_CODE']);
+```
+
+**After (2025-10):**
+```typescript
+await cart.addGiftCardCodes(['NEW_CODE']);
+```
+
+## Verified API Behavior
+
+| Scenario | Behavior |
+|----------|----------|
+| Valid gift card code | Applied successfully |
+| UPPERCASE code | Works (API is case-insensitive) |
+| Duplicate code in same call | Idempotent - applied once, no error |
+| Re-applying already applied code | Idempotent - no error, no duplicate |
+| Multiple different codes | All applied successfully |
+| Invalid code | Silently rejected (no error surfaced) |
+| Code with whitespace | Rejected (API does not trim whitespace) |
+| Empty input | Graceful no-op |
+
+**Note:** The API handles duplicate gift card codes gracefully - submitting an already-applied code results in silent success (idempotent behavior), not an error. No `DUPLICATE_GIFT_CARD` error code exists.
+
+**Note on whitespace:** The API does NOT trim whitespace from codes. Ensure codes are trimmed before submission if accepting user input.
+
+## API Reference
+
+**New method:**
+- `cart.addGiftCardCodes(codes)` - Appends codes to cart
+- `CartForm.ACTIONS.GiftCardCodesAdd` - Form action
+
+## Skeleton Template Changes
+
+The skeleton template has been updated to use the new `cartGiftCardCodesAdd` mutation:
+- Removed `UpdateGiftCardForm` component from `CartSummary.tsx`
+- Added `AddGiftCardForm` component using `CartForm.ACTIONS.GiftCardCodesAdd`
+
+If you customized the gift card form in your project, you may want to migrate to the new `Add` action for simpler code.
+
+## Usage
+
+```typescript
+import {CartForm} from '@shopify/hydrogen';
+
+
+
+
+```
+
+Or with createCartHandler:
+
+```typescript
+const cart = createCartHandler({storefront, getCartId, setCartId});
+await cart.addGiftCardCodes(['SUMMER2025', 'WELCOME10']);
+```
diff --git a/packages/hydrogen/src/cart/CartForm.test.tsx b/packages/hydrogen/src/cart/CartForm.test.tsx
index e581ce0114..c35831e885 100644
--- a/packages/hydrogen/src/cart/CartForm.test.tsx
+++ b/packages/hydrogen/src/cart/CartForm.test.tsx
@@ -87,6 +87,7 @@ describe('', () => {
DeliveryAddressesRemove: 'DeliveryAddressesRemove',
DiscountCodesUpdate: 'DiscountCodesUpdate',
GiftCardCodesUpdate: 'GiftCardCodesUpdate',
+ GiftCardCodesAdd: 'GiftCardCodesAdd',
GiftCardCodesRemove: 'GiftCardCodesRemove',
LinesAdd: 'LinesAdd',
LinesUpdate: 'LinesUpdate',
diff --git a/packages/hydrogen/src/cart/CartForm.tsx b/packages/hydrogen/src/cart/CartForm.tsx
index 5e5d92c985..6b03147fd6 100644
--- a/packages/hydrogen/src/cart/CartForm.tsx
+++ b/packages/hydrogen/src/cart/CartForm.tsx
@@ -87,6 +87,20 @@ type CartGiftCardCodesUpdateRequire = {
} & OtherFormData;
};
+type CartGiftCardCodesAddProps = {
+ action: 'GiftCardCodesAdd';
+ inputs?: {
+ giftCardCodes: string[];
+ } & OtherFormData;
+};
+
+type CartGiftCardCodesAddRequire = {
+ action: 'GiftCardCodesAdd';
+ inputs: {
+ giftCardCodes: string[];
+ } & OtherFormData;
+};
+
type CartGiftCardCodesRemoveProps = {
action: 'GiftCardCodesRemove';
inputs?: {
@@ -278,6 +292,7 @@ type CartActionInputProps =
| CartCreateProps
| CartDiscountCodesUpdateProps
| CartGiftCardCodesUpdateProps
+ | CartGiftCardCodesAddProps
| CartGiftCardCodesRemoveProps
| CartLinesAddProps
| CartLinesUpdateProps
@@ -297,6 +312,7 @@ export type CartActionInput =
| CartCreateRequire
| CartDiscountCodesUpdateRequire
| CartGiftCardCodesUpdateRequire
+ | CartGiftCardCodesAddRequire
| CartGiftCardCodesRemoveRequire
| CartLinesAddRequire
| CartLinesUpdateRequire
@@ -345,6 +361,7 @@ CartForm.ACTIONS = {
Create: 'Create',
DiscountCodesUpdate: 'DiscountCodesUpdate',
GiftCardCodesUpdate: 'GiftCardCodesUpdate',
+ GiftCardCodesAdd: 'GiftCardCodesAdd',
GiftCardCodesRemove: 'GiftCardCodesRemove',
LinesAdd: 'LinesAdd',
LinesRemove: 'LinesRemove',
diff --git a/packages/hydrogen/src/cart/createCartHandler.test.ts b/packages/hydrogen/src/cart/createCartHandler.test.ts
index 1d3a0318bb..f0f932c1a7 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');
@@ -47,6 +47,7 @@ describe('createCartHandler', () => {
expect(cart).toHaveProperty('removeLines');
expect(cart).toHaveProperty('updateDiscountCodes');
expect(cart).toHaveProperty('updateGiftCardCodes');
+ expect(cart).toHaveProperty('addGiftCardCodes');
expect(cart).toHaveProperty('removeGiftCardCodes');
expect(cart).toHaveProperty('updateBuyerIdentity');
expect(cart).toHaveProperty('updateNote');
@@ -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..4f653f4239 100644
--- a/packages/hydrogen/src/cart/createCartHandler.ts
+++ b/packages/hydrogen/src/cart/createCartHandler.ts
@@ -49,6 +49,10 @@ import {
type CartGiftCardCodesUpdateFunction,
cartGiftCardCodesUpdateDefault,
} from './queries/cartGiftCardCodeUpdateDefault';
+import {
+ type CartGiftCardCodesAddFunction,
+ cartGiftCardCodesAddDefault,
+} from './queries/cartGiftCardCodesAddDefault';
import {
type CartGiftCardCodesRemoveFunction,
cartGiftCardCodesRemoveDefault,
@@ -94,6 +98,7 @@ export type HydrogenCart = {
removeLines: ReturnType;
updateDiscountCodes: ReturnType;
updateGiftCardCodes: ReturnType;
+ addGiftCardCodes: ReturnType;
removeGiftCardCodes: ReturnType;
updateBuyerIdentity: ReturnType;
updateNote: ReturnType;
@@ -274,6 +279,7 @@ export function createCartHandler(
)
: await cartCreate({giftCardCodes}, optionalParams);
},
+ addGiftCardCodes: cartGiftCardCodesAddDefault(mutateOptions),
removeGiftCardCodes: cartGiftCardCodesRemoveDefault(mutateOptions),
updateBuyerIdentity: async (buyerIdentity, optionalParams) => {
return cartId || optionalParams?.cartId
@@ -425,6 +431,10 @@ export type HydrogenCartForDocs = {
* Updates gift card codes in the cart.
*/
updateGiftCardCodes?: CartGiftCardCodesUpdateFunction;
+ /**
+ * Adds gift card codes to the cart without replacing existing ones.
+ */
+ addGiftCardCodes?: CartGiftCardCodesAddFunction;
/**
* Removes gift card codes from the cart.
*/
diff --git a/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.doc.ts b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.doc.ts
new file mode 100644
index 0000000000..aa13f98609
--- /dev/null
+++ b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.doc.ts
@@ -0,0 +1,56 @@
+import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
+
+const data: ReferenceEntityTemplateSchema = {
+ name: 'cartGiftCardCodesAddDefault',
+ category: 'utilities',
+ subCategory: 'cart',
+ isVisualComponent: false,
+ related: [
+ {
+ name: 'cartGiftCardCodesUpdateDefault',
+ type: 'utilities',
+ url: '/docs/api/hydrogen/utilities/cartgiftcardcodesupdatedefault',
+ },
+ {
+ name: 'cartGiftCardCodesRemoveDefault',
+ type: 'utilities',
+ url: '/docs/api/hydrogen/utilities/cartgiftcardcodesremovedefault',
+ },
+ {
+ name: 'createCartHandler',
+ type: 'utilities',
+ url: '/docs/api/hydrogen/utilities/createcarthandler',
+ },
+ ],
+ description:
+ 'Creates a function that adds gift card codes to a cart without replacing existing ones',
+ type: 'utility',
+ defaultExample: {
+ description:
+ 'Add gift card codes to a cart using the default cart fragment',
+ codeblock: {
+ tabs: [
+ {
+ title: 'JavaScript',
+ code: './cartGiftCardCodesAddDefault.example.js',
+ language: 'js',
+ },
+ {
+ title: 'TypeScript',
+ code: './cartGiftCardCodesAddDefault.example.ts',
+ language: 'ts',
+ },
+ ],
+ title: 'example',
+ },
+ },
+ definitions: [
+ {
+ title: 'cartGiftCardCodesAddDefault',
+ type: 'CartGiftCardCodesAddDefaultGeneratedType',
+ description: '',
+ },
+ ],
+};
+
+export default data;
diff --git a/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.js b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.js
new file mode 100644
index 0000000000..6a1bf78ebd
--- /dev/null
+++ b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.js
@@ -0,0 +1,11 @@
+import {cartGiftCardCodesAddDefault} from '@shopify/hydrogen';
+
+export async function action({context}) {
+ const cartAddGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: context.storefront,
+ getCartId: () => context.cart.getCartId(),
+ });
+
+ const result = await cartAddGiftCardCodes(['SUMMER2025', 'WELCOME10']);
+ return result;
+}
diff --git a/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.ts b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.ts
new file mode 100644
index 0000000000..dfd16595f1
--- /dev/null
+++ b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.example.ts
@@ -0,0 +1,16 @@
+import {
+ cartGiftCardCodesAddDefault,
+ type HydrogenCart,
+ type CartQueryOptions,
+} from '@shopify/hydrogen';
+
+export async function action({context}: {context: CartQueryOptions}) {
+ const cartAddGiftCardCodes: HydrogenCart['addGiftCardCodes'] =
+ cartGiftCardCodesAddDefault({
+ storefront: context.storefront,
+ getCartId: context.getCartId,
+ });
+
+ const result = await cartAddGiftCardCodes(['SUMMER2025', 'WELCOME10']);
+ return result;
+}
diff --git a/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.test.ts b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.test.ts
new file mode 100644
index 0000000000..eaa02bbb51
--- /dev/null
+++ b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.test.ts
@@ -0,0 +1,102 @@
+/**
+ * TODO: These tests are placeholders that verify mock returns, not actual API behavior.
+ * They should be improved in a follow-up PR to test real integration scenarios.
+ * See PR #3284 for context.
+ */
+import {describe, it, expect} from 'vitest';
+import {CART_ID, mockCreateStorefrontClient} from '../cart-test-helper';
+import {
+ cartGiftCardCodesAddDefault,
+ CART_GIFT_CARD_CODES_ADD_MUTATION,
+} from './cartGiftCardCodesAddDefault';
+
+describe('cartGiftCardCodesAddDefault', () => {
+ describe('basic functionality', () => {
+ it('should add gift card codes to cart without replacing existing ones', async () => {
+ const addGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: mockCreateStorefrontClient(),
+ getCartId: () => CART_ID,
+ });
+
+ const result = await addGiftCardCodes(['SUMMER2025']);
+
+ expect(result.cart).toHaveProperty('id', CART_ID);
+ });
+
+ it('should handle multiple gift card codes in single call', async () => {
+ const addGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: mockCreateStorefrontClient(),
+ getCartId: () => CART_ID,
+ });
+
+ const result = await addGiftCardCodes([
+ 'GIFT123',
+ 'GIFT456',
+ 'WELCOME25',
+ ]);
+
+ expect(result.cart).toHaveProperty('id', CART_ID);
+ });
+
+ it('should handle empty array', async () => {
+ const addGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: mockCreateStorefrontClient(),
+ getCartId: () => CART_ID,
+ });
+
+ const result = await addGiftCardCodes([]);
+
+ expect(result.cart).toHaveProperty('id', CART_ID);
+ });
+ });
+
+ describe('cartFragment override', () => {
+ it('can override cartFragment for custom query fields', async () => {
+ const cartFragment = 'cartFragmentOverride';
+ const addGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: mockCreateStorefrontClient(),
+ getCartId: () => CART_ID,
+ cartFragment,
+ });
+
+ const result = await addGiftCardCodes(['TESTCODE']);
+
+ expect(result.cart).toHaveProperty('id', CART_ID);
+ expect(result.userErrors?.[0]).toContain(cartFragment);
+ });
+ });
+
+ describe('mutation structure', () => {
+ it('should include required mutation fields for error and warning handling', () => {
+ const mutation = CART_GIFT_CARD_CODES_ADD_MUTATION();
+
+ expect(mutation).toContain('cartGiftCardCodesAdd');
+ expect(mutation).toContain('userErrors');
+ expect(mutation).toContain('warnings');
+ expect(mutation).toContain('CartApiError');
+ expect(mutation).toContain('CartApiWarning');
+ });
+
+ it('should include @inContext directive for internationalization', () => {
+ const mutation = CART_GIFT_CARD_CODES_ADD_MUTATION();
+
+ expect(mutation).toContain('@inContext');
+ expect(mutation).toContain('$country');
+ expect(mutation).toContain('$language');
+ });
+ });
+
+ describe('no duplicate filtering', () => {
+ it('should pass duplicate codes to API without filtering', async () => {
+ const addGiftCardCodes = cartGiftCardCodesAddDefault({
+ storefront: mockCreateStorefrontClient(),
+ getCartId: () => CART_ID,
+ });
+
+ const codesWithDuplicates = ['GIFT123', 'GIFT123', 'GIFT456'];
+ const result = await addGiftCardCodes(codesWithDuplicates);
+
+ expect(result.cart).toHaveProperty('id', CART_ID);
+ });
+ });
+});
diff --git a/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.ts b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.ts
new file mode 100644
index 0000000000..fecb9b85be
--- /dev/null
+++ b/packages/hydrogen/src/cart/queries/cartGiftCardCodesAddDefault.ts
@@ -0,0 +1,75 @@
+import {StorefrontApiErrors, formatAPIResult} from '../../storefront';
+import {
+ CART_WARNING_FRAGMENT,
+ MINIMAL_CART_FRAGMENT,
+ USER_ERROR_FRAGMENT,
+} from './cart-fragments';
+import type {
+ CartOptionalInput,
+ CartQueryData,
+ CartQueryDataReturn,
+ CartQueryOptions,
+} from './cart-types';
+
+export type CartGiftCardCodesAddFunction = (
+ giftCardCodes: string[],
+ optionalParams?: CartOptionalInput,
+) => Promise;
+
+/**
+ * Adds gift card codes to the cart without replacing existing ones.
+ *
+ * This function sends a mutation to the Storefront API to add one or more gift card codes to the cart.
+ * Unlike `cartGiftCardCodesUpdate` which replaces all codes, this mutation appends new codes to existing ones.
+ *
+ * @param {CartQueryOptions} options - The options for the cart query, including the storefront API client and cart fragment.
+ * @returns {CartGiftCardCodesAddFunction} - A function that takes an array of gift card codes and optional parameters, and returns the result of the API call.
+ *
+ * @example Add gift card codes
+ * const addGiftCardCodes = cartGiftCardCodesAddDefault({ storefront, getCartId });
+ * await addGiftCardCodes(['SUMMER2025', 'WELCOME10']);
+ */
+export function cartGiftCardCodesAddDefault(
+ options: CartQueryOptions,
+): CartGiftCardCodesAddFunction {
+ return async (giftCardCodes, optionalParams) => {
+ const {cartGiftCardCodesAdd, errors} = await options.storefront.mutate<{
+ cartGiftCardCodesAdd: CartQueryData;
+ errors: StorefrontApiErrors;
+ }>(CART_GIFT_CARD_CODES_ADD_MUTATION(options.cartFragment), {
+ variables: {
+ cartId: options.getCartId(),
+ giftCardCodes,
+ ...optionalParams,
+ },
+ });
+ return formatAPIResult(cartGiftCardCodesAdd, errors);
+ };
+}
+
+//! @see https://shopify.dev/docs/api/storefront/latest/mutations/cartGiftCardCodesAdd
+export const CART_GIFT_CARD_CODES_ADD_MUTATION = (
+ cartFragment = MINIMAL_CART_FRAGMENT,
+) => `#graphql
+ mutation cartGiftCardCodesAdd(
+ $cartId: ID!
+ $giftCardCodes: [String!]!
+ $language: LanguageCode
+ $country: CountryCode
+ ) @inContext(country: $country, language: $language) {
+ cartGiftCardCodesAdd(cartId: $cartId, giftCardCodes: $giftCardCodes) {
+ cart {
+ ...CartApiMutation
+ }
+ userErrors {
+ ...CartApiError
+ }
+ warnings {
+ ...CartApiWarning
+ }
+ }
+ }
+ ${cartFragment}
+ ${USER_ERROR_FRAGMENT}
+ ${CART_WARNING_FRAGMENT}
+`;
diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts
index 2b14a01f54..5dadfadecb 100644
--- a/packages/hydrogen/src/index.ts
+++ b/packages/hydrogen/src/index.ts
@@ -58,6 +58,7 @@ export {cartBuyerIdentityUpdateDefault} from './cart/queries/cartBuyerIdentityUp
export {cartCreateDefault} from './cart/queries/cartCreateDefault';
export {cartDiscountCodesUpdateDefault} from './cart/queries/cartDiscountCodesUpdateDefault';
export {cartGetDefault} from './cart/queries/cartGetDefault';
+export {cartGiftCardCodesAddDefault} from './cart/queries/cartGiftCardCodesAddDefault';
export {cartGiftCardCodesRemoveDefault} from './cart/queries/cartGiftCardCodesRemoveDefault';
export {cartGiftCardCodesUpdateDefault} from './cart/queries/cartGiftCardCodeUpdateDefault';
export {cartLinesAddDefault} from './cart/queries/cartLinesAddDefault';
diff --git a/templates/skeleton/app/components/CartSummary.tsx b/templates/skeleton/app/components/CartSummary.tsx
index 541f2c819d..408fb9895c 100644
--- a/templates/skeleton/app/components/CartSummary.tsx
+++ b/templates/skeleton/app/components/CartSummary.tsx
@@ -149,8 +149,7 @@ function CartGiftCard({
)}
{/* Show an input to apply a gift card */}
-
@@ -166,18 +165,16 @@ function CartGiftCard({
Apply
-
+
);
}
-function UpdateGiftCardForm({
- giftCardCodes,
+function AddGiftCardForm({
saveAppliedCode,
fetcherKey,
children,
}: {
- giftCardCodes?: string[];
saveAppliedCode?: (code: string) => void;
fetcherKey?: string;
children: React.ReactNode;
@@ -186,10 +183,7 @@ function UpdateGiftCardForm({
{(fetcher: FetcherWithComponents) => {
const code = fetcher.formData?.get('giftCardCode');
diff --git a/templates/skeleton/app/routes/cart.tsx b/templates/skeleton/app/routes/cart.tsx
index f82d683fdb..bb143b40d9 100644
--- a/templates/skeleton/app/routes/cart.tsx
+++ b/templates/skeleton/app/routes/cart.tsx
@@ -52,18 +52,14 @@ export async function action({request, context}: Route.ActionArgs) {
result = await cart.updateDiscountCodes(discountCodes);
break;
}
- case CartForm.ACTIONS.GiftCardCodesUpdate: {
+ case CartForm.ACTIONS.GiftCardCodesAdd: {
const formGiftCardCode = inputs.giftCardCode;
- // User inputted gift card code
const giftCardCodes = (
formGiftCardCode ? [formGiftCardCode] : []
) as string[];
- // Combine gift card codes already applied on cart
- giftCardCodes.push(...inputs.giftCardCodes);
-
- result = await cart.updateGiftCardCodes(giftCardCodes);
+ result = await cart.addGiftCardCodes(giftCardCodes);
break;
}
case CartForm.ACTIONS.GiftCardCodesRemove: {