-
Notifications
You must be signed in to change notification settings - Fork 380
[2025-10] Add cartGiftCardCodesAdd mutation and remove Update filtering #3284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3887a03
6882644
7c8eb92
a9898cd
e4ae789
517276b
bdc0962
48af6de
65e91b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| --- | ||
| '@shopify/hydrogen': major | ||
| --- | ||
|
|
||
| Add `cartGiftCardCodesAdd` mutation and remove duplicate filtering from `cartGiftCardCodesUpdate` | ||
|
|
||
| ## 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']); | ||
| ``` | ||
|
|
||
| ## Breaking Change: cartGiftCardCodesUpdate | ||
|
|
||
| Removed client-side duplicate code filtering. The Storefront API handles duplicates gracefully. | ||
|
|
||
| **What changed:** | ||
| - Previously: Hydrogen filtered duplicate codes before sending to API | ||
| - Now: Codes pass directly to API (thin wrapper pattern) | ||
|
|
||
| **Why:** | ||
| - API is idempotent for duplicate codes (verified via E2E testing) | ||
| - Consistent with all Add/Remove mutations (no filtering) | ||
| - Filtering was legacy code copied from discount codes | ||
|
|
||
| ## Verified API Behavior (E2E tested 2026-01-21) | ||
|
|
||
| | 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 | | ||
|
|
||
| **Key finding:** 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. | ||
|
|
||
| ## Migration | ||
|
|
||
| **Most users are unaffected.** The API handles duplicates gracefully. | ||
|
|
||
| If you rely on client-side deduplication for other reasons (e.g., avoiding unnecessary API calls), filter codes before calling: | ||
|
|
||
| ```typescript | ||
| const uniqueCodes = [...new Set(codes)]; | ||
| await cart.updateGiftCardCodes(uniqueCodes); | ||
| ``` | ||
|
|
||
| **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 | ||
|
|
||
| **Changed method:** | ||
| - `cart.updateGiftCardCodes(codes)` - No longer filters duplicates | ||
|
|
||
| ## 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'; | ||
|
|
||
| <CartForm action={CartForm.ACTIONS.GiftCardCodesAdd} inputs={{giftCardCodes: ['CODE1', 'CODE2']}}> | ||
| <button>Add Gift Cards</button> | ||
| </CartForm> | ||
| ``` | ||
|
|
||
| Or with createCartHandler: | ||
|
|
||
| ```typescript | ||
| const cart = createCartHandler({storefront, getCartId, setCartId}); | ||
| await cart.addGiftCardCodes(['SUMMER2025', 'WELCOME10']); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -16,22 +16,30 @@ export type CartGiftCardCodesUpdateFunction = ( | |||
| optionalParams?: CartOptionalInput, | ||||
| ) => Promise<CartQueryDataReturn>; | ||||
|
|
||||
| /** | ||||
| * Updates (replaces) gift card codes in the cart. | ||||
| * | ||||
| * This function no longer filters duplicate codes internally. | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
| * To add codes without replacing, use `cartGiftCardCodesAdd` (API 2025-10+). | ||||
| * | ||||
| * @param {CartQueryOptions} options - Cart query options including storefront client and cart fragment. | ||||
| * @returns {CartGiftCardCodesUpdateFunction} - Function accepting gift card codes array and optional parameters. | ||||
| * | ||||
| * @example Replace all gift card codes | ||||
| * const updateGiftCardCodes = cartGiftCardCodesUpdateDefault({ storefront, getCartId }); | ||||
| * await updateGiftCardCodes(['SUMMER2025', 'WELCOME10']); | ||||
| */ | ||||
| export function cartGiftCardCodesUpdateDefault( | ||||
| options: CartQueryOptions, | ||||
| ): CartGiftCardCodesUpdateFunction { | ||||
| return async (giftCardCodes, optionalParams) => { | ||||
| // Ensure the gift card codes are unique | ||||
| const uniqueCodes = giftCardCodes.filter((value, index, array) => { | ||||
| return array.indexOf(value) === index; | ||||
| }); | ||||
|
|
||||
| const {cartGiftCardCodesUpdate, errors} = await options.storefront.mutate<{ | ||||
| cartGiftCardCodesUpdate: CartQueryData; | ||||
| errors: StorefrontApiErrors; | ||||
| }>(CART_GIFT_CARD_CODE_UPDATE_MUTATION(options.cartFragment), { | ||||
| variables: { | ||||
| cartId: options.getCartId(), | ||||
| giftCardCodes: uniqueCodes, | ||||
| giftCardCodes, | ||||
| ...optionalParams, | ||||
| }, | ||||
| }); | ||||
|
|
||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my own learning: why do all of these have
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 'Default' suffix indicates this is the default implementation. Custom implementations can be provided via createCartHandler options (eg: to use a custom cart fragment) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm waffling on this being considered a breaking change since it's so minimal but I guess it is a change in behaviour so we need to mark it as a break?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, Hyrum's law
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I dug into this more - I was under the impression that Hydrogen's previous logic when calling this mutation was to support incrementally adding gift card codes, and now that we have a gift cards add mutation we want to use that to incrementally add gift cards and switch the update mutation back to being a "replace" mutation.
But that wasn't the case. Hydrogen was just filtering out duplicates before passing them to the SF API, but the SF API mutation is idempotent and ignores duplicates, so Hydrogen's logic is unnecessary and can be removed. This isn't coupled to the gift cards add mutation at all. So:
The function signature is unchanged, the return type is unchanged, and the observable outcome (which gift cards end up on the cart) is unchanged. The filtering was purely internal. If the API is idempotent for duplicates, consumers calling updateGiftCardCodes(['A', 'A', 'B']) get the same result as before.
Therefore this is NOT a breaking change and just an internal refactor with no side effects