[2025-10] Add cartGiftCardCodesAdd mutation and remove Update filtering
WHY are these changes introduced?
Fixes #3271
Storefront API 2025-10 added cartGiftCardCodesAdd mutation for appending gift card codes without replacing existing ones. Hydrogen only implemented cartGiftCardCodesRemove in PR #3128, leaving users without a way to add codes incrementally. Since the API only returns the last 4 digits of applied gift cards (security constraint), users cannot fetch existing codes to preserve them when using the Update mutation.
This PR also removes legacy duplicate filtering from cartGiftCardCodesUpdate to align with API 2025-10 thin wrapper architecture.
WHAT is this pull request doing?
New Feature: cartGiftCardCodesAdd
Adds thin wrapper for cartGiftCardCodesAdd mutation following the established Add mutation pattern.
Files created:
cartGiftCardCodesAddDefault.ts- Core implementation (no duplicate filtering)cartGiftCardCodesAddDefault.test.ts- 7 comprehensive testscartGiftCardCodesAddDefault.doc.ts- Documentation metadatacartGiftCardCodesAddDefault.example.js/ts- Usage examples
Integration:
- Exported from
createCartHandlerasaddGiftCardCodes - Added
CartForm.ACTIONS.GiftCardCodesAddaction type - Exported from package index
Usage:
// Using createCartHandler
const cart = createCartHandler({storefront, getCartId, setCartId});
await cart.addGiftCardCodes(['SUMMER2025', 'WELCOME10']);
// Using CartForm
<CartForm
action={CartForm.ACTIONS.GiftCardCodesAdd}
inputs={{giftCardCodes: ['SUMMER2025']}}
>
<button>Apply Gift Card</button>
</CartForm>
Breaking Change: cartGiftCardCodesUpdate
Removed client-side duplicate code filtering to align with thin wrapper pattern.
Before:
// Hydrogen filtered unique codes before API call
const uniqueCodes = giftCardCodes.filter((value, index, array) =>
array.indexOf(value) === index
);
// Only unique codes sent to API
After:
// Codes pass directly to API
const {cartGiftCardCodesUpdate, errors} = await storefront.mutate(
MUTATION,
{ variables: { giftCardCodes } }
);
Architecture Decision:
| Mutation Type | Filtering | Count | Pattern |
|---|---|---|---|
| Add mutations | None | 3/3 (100%) | Thin wrapper |
| Remove mutations | None | 3/3 (100%) | Thin wrapper |
| Update mutations | Changed | 1/3 (33%) | Now thin wrapper |
Migration: If you need client-side deduplication:
const uniqueCodes = codes.filter((v, i, a) => a.indexOf(v) === i);
await cart.updateGiftCardCodes(uniqueCodes);
HOW to test your changes?
🎩 Top Hat
Prerequisites
- [ ] Hydrogen project on 2025-10 API version
- [ ] Storefront with gift card products enabled
- [ ] Test gift card codes ready
Testing Steps
Feature 1: Add Gift Card Codes
- Setup test environment:
npm create @shopify/hydrogen@latest
cd your-project
# Ensure API version is 2025-10 in .env
- Create test route (
app/routes/test-gift-cards.tsx):
import {CartForm} from '@shopify/hydrogen';
export default function TestGiftCards() {
return (
<div>
<h1>Test Gift Card Add</h1>
{/* Test 1: Add single code */}
<CartForm
action={CartForm.ACTIONS.GiftCardCodesAdd}
inputs={{giftCardCodes: ['TESTCODE1']}}
>
<button>Add Single Code</button>
</CartForm>
{/* Test 2: Add multiple codes */}
<CartForm
action={CartForm.ACTIONS.GiftCardCodesAdd}
inputs={{giftCardCodes: ['CODE1', 'CODE2']}}
>
<button>Add Multiple Codes</button>
</CartForm>
{/* Test 3: Add duplicate codes (should not filter) */}
<CartForm
action={CartForm.ACTIONS.GiftCardCodesAdd}
inputs={{giftCardCodes: ['DUP', 'DUP', 'UNIQUE']}}
>
<button>Add with Duplicates</button>
</CartForm>
</div>
);
}
- Test with createCartHandler (
app/routes/api.gift-cards.tsx):
import {createCartHandler} from '@shopify/hydrogen';
export async function action({context}) {
const cart = createCartHandler({
storefront: context.storefront,
getCartId: context.session.get('cartId'),
setCartId: (cartId) => context.session.set('cartId', cartId),
});
// Test: Add codes without replacing existing
const result = await cart.addGiftCardCodes(['SUMMER2025', 'WELCOME10']);
return json(result);
}
- Expected behavior:
- Codes append to existing gift cards
- Existing codes remain in cart
- API handles any duplicate normalization
- No client-side filtering
Feature 2: Update No Longer Filters
- Test duplicate handling:
// Duplicates now pass to API
await cart.updateGiftCardCodes(['CODE1', 'CODE1', 'CODE2']);
// Previously would filter to ['CODE1', 'CODE2']
// Now passes ['CODE1', 'CODE1', 'CODE2'] to API
- Expected behavior:
- API receives all codes including duplicates
- API handles case-insensitive normalization
- No console errors
- Cart updates successfully
Edge Cases to Test
- [ ] Empty array to Add (should be no-op)
- [ ] Very long code strings (>50 chars)
- [ ] Special characters in codes
- [ ] Case variations (GIFT123 vs gift123)
- [ ] Existing + new codes (verify append)
- [ ] Invalid gift card code (API should return error)
Validation Checklist
- [x] All tests pass locally (447 tests)
- [x] TypeScript clean (no errors)
- [x] Lint passes
- [x] Changeset created
- [x] Documentation added (doc.ts, examples)
- [x] Investigation documented (investigation-3271.md)
Checklist
- [x] I've read the Contributing Guidelines
- [x] I've considered possible cross-platform impacts (Mac, Linux, Windows)
- [x] I've added a changeset if this PR contains user-facing or noteworthy changes
- [x] I've added tests to cover my changes
- [x] I've added or updated the documentation
Summary of Changes
New:
cart.addGiftCardCodes(codes)- Append codes without replacingCartForm.ACTIONS.GiftCardCodesAdd- Form action
Breaking:
cart.updateGiftCardCodes(codes)- No longer filters duplicates client-side
Files: 13 files, +400/-9 lines
Tests: 7 new tests, all passing
Architecture: 100% thin wrapper consistency (was 78%)