Skip to content

Commit ad445a5

Browse files
committed
feat(cart): add visitorConsent support to @incontext directive
Enables privacy-compliant checkouts by adding visitorConsent parameter to all cart operations. When provided, buyer consent preferences (analytics, marketing, preferences, saleOfData) are encoded into the cart's checkoutUrl via the _cs parameter. - Add visitorConsent to CartOptionalInput type following country/language pattern - Update all 17 Hydrogen cart GraphQL operations with @incontext directive support - Update 9 hydrogen-react cart queries - Add toStorefrontVisitorConsent() utility for Customer Privacy API conversion - Add unit tests for the conversion utility
1 parent bce6ffc commit ad445a5

23 files changed

Lines changed: 235 additions & 27 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@shopify/hydrogen': minor
3+
'@shopify/hydrogen-react': minor
4+
---
5+
6+
Add `visitorConsent` support to `@inContext` directive for privacy-compliant checkouts
7+
8+
Adds support for the Storefront API 2025-10 `visitorConsent` parameter in `@inContext` directives. When provided, buyer consent preferences (analytics, marketing, preferences, saleOfData) are automatically encoded into the cart's `checkoutUrl` via the `_cs` parameter.
9+
10+
**New Features:**
11+
- All cart operations now accept an optional `visitorConsent` parameter via `CartOptionalInput`
12+
- New `toStorefrontVisitorConsent()` utility converts Customer Privacy API consent format (snake_case) to Storefront API format (camelCase)
13+
14+
**Usage:**
15+
```tsx
16+
import {useCustomerPrivacy, toStorefrontVisitorConsent} from '@shopify/hydrogen';
17+
18+
function AddToCart() {
19+
const {customerPrivacy} = useCustomerPrivacy({...});
20+
21+
async function handleAddToCart() {
22+
const consent = customerPrivacy?.currentVisitorConsent();
23+
await cart.addLines(lines, {
24+
visitorConsent: consent ? toStorefrontVisitorConsent(consent) : undefined
25+
});
26+
}
27+
}
28+
```

packages/hydrogen-react/src/cart-queries.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export const CartLineAdd = (cartFragment: string): string => /* GraphQL */ `
55
$numCartLines: Int = 250
66
$country: CountryCode = ZZ
77
$language: LanguageCode
8-
) @inContext(country: $country, language: $language) {
8+
$visitorConsent: VisitorConsent
9+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
910
cartLinesAdd(cartId: $cartId, lines: $lines) {
1011
cart {
1112
...CartFragment
@@ -22,7 +23,8 @@ export const CartCreate = (cartFragment: string): string => /* GraphQL */ `
2223
$numCartLines: Int = 250
2324
$country: CountryCode = ZZ
2425
$language: LanguageCode
25-
) @inContext(country: $country, language: $language) {
26+
$visitorConsent: VisitorConsent
27+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
2628
cartCreate(input: $input) {
2729
cart {
2830
...CartFragment
@@ -40,7 +42,8 @@ export const CartLineRemove = (cartFragment: string): string => /* GraphQL */ `
4042
$numCartLines: Int = 250
4143
$country: CountryCode = ZZ
4244
$language: LanguageCode
43-
) @inContext(country: $country, language: $language) {
45+
$visitorConsent: VisitorConsent
46+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
4447
cartLinesRemove(cartId: $cartId, lineIds: $lines) {
4548
cart {
4649
...CartFragment
@@ -58,7 +61,8 @@ export const CartLineUpdate = (cartFragment: string): string => /* GraphQL */ `
5861
$numCartLines: Int = 250
5962
$country: CountryCode = ZZ
6063
$language: LanguageCode
61-
) @inContext(country: $country, language: $language) {
64+
$visitorConsent: VisitorConsent
65+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
6266
cartLinesUpdate(cartId: $cartId, lines: $lines) {
6367
cart {
6468
...CartFragment
@@ -76,7 +80,8 @@ export const CartNoteUpdate = (cartFragment: string): string => /* GraphQL */ `
7680
$numCartLines: Int = 250
7781
$country: CountryCode = ZZ
7882
$language: LanguageCode
79-
) @inContext(country: $country, language: $language) {
83+
$visitorConsent: VisitorConsent
84+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
8085
cartNoteUpdate(cartId: $cartId, note: $note) {
8186
cart {
8287
...CartFragment
@@ -96,7 +101,8 @@ export const CartBuyerIdentityUpdate = (
96101
$numCartLines: Int = 250
97102
$country: CountryCode = ZZ
98103
$language: LanguageCode
99-
) @inContext(country: $country, language: $language) {
104+
$visitorConsent: VisitorConsent
105+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
100106
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
101107
cart {
102108
...CartFragment
@@ -116,7 +122,8 @@ export const CartAttributesUpdate = (
116122
$numCartLines: Int = 250
117123
$country: CountryCode = ZZ
118124
$language: LanguageCode
119-
) @inContext(country: $country, language: $language) {
125+
$visitorConsent: VisitorConsent
126+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
120127
cartAttributesUpdate(attributes: $attributes, cartId: $cartId) {
121128
cart {
122129
...CartFragment
@@ -136,7 +143,8 @@ export const CartDiscountCodesUpdate = (
136143
$numCartLines: Int = 250
137144
$country: CountryCode = ZZ
138145
$language: LanguageCode
139-
) @inContext(country: $country, language: $language) {
146+
$visitorConsent: VisitorConsent
147+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
140148
cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
141149
cart {
142150
...CartFragment
@@ -153,7 +161,8 @@ export const CartQuery = (cartFragment: string): string => /* GraphQL */ `
153161
$numCartLines: Int = 250
154162
$country: CountryCode = ZZ
155163
$language: LanguageCode
156-
) @inContext(country: $country, language: $language) {
164+
$visitorConsent: VisitorConsent
165+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
157166
cart(id: $id) {
158167
...CartFragment
159168
}

packages/hydrogen/src/cart/queries/cart-types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
MetafieldsSetUserError,
99
MetafieldDeleteUserError,
1010
CartWarning,
11+
VisitorConsent,
1112
} from '@shopify/hydrogen-react/storefront-api-types';
1213
import type {StorefrontApiErrors, Storefront} from '../../storefront';
1314
import {CustomerAccount} from '../../customer/types';
@@ -28,6 +29,14 @@ export type CartOptionalInput = {
2829
* @default storefront.i18n.language
2930
*/
3031
language?: LanguageCode;
32+
/**
33+
* Visitor consent preferences for privacy-compliant checkouts.
34+
* When provided, consent is encoded into the cart's checkoutUrl via the _cs parameter.
35+
* Unlike country/language which are typically static, consent can change during a session
36+
* as users interact with privacy banners - always pass the current consent state.
37+
* @see https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/in-context
38+
*/
39+
visitorConsent?: VisitorConsent;
3140
};
3241

3342
export type MetafieldWithoutOwnerId = Omit<CartMetafieldsSetInput, 'ownerId'>;

packages/hydrogen/src/cart/queries/cartAttributesUpdateDefault.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function cartAttributesUpdateDefault(
2828
variables: {
2929
cartId: optionalParams?.cartId || options.getCartId(),
3030
attributes,
31+
...optionalParams,
3132
},
3233
});
3334
return formatAPIResult(cartAttributesUpdate, errors);
@@ -40,7 +41,10 @@ export const CART_ATTRIBUTES_UPDATE_MUTATION = (
4041
mutation cartAttributesUpdate(
4142
$cartId: ID!
4243
$attributes: [AttributeInput!]!
43-
) {
44+
$language: LanguageCode
45+
$country: CountryCode
46+
$visitorConsent: VisitorConsent
47+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
4448
cartAttributesUpdate(cartId: $cartId, attributes: $attributes) {
4549
cart {
4650
...CartApiMutation

packages/hydrogen/src/cart/queries/cartBuyerIdentityUpdateDefault.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export const CART_BUYER_IDENTITY_UPDATE_MUTATION = (
5757
$buyerIdentity: CartBuyerIdentityInput!
5858
$language: LanguageCode
5959
$country: CountryCode
60-
) @inContext(country: $country, language: $language) {
60+
$visitorConsent: VisitorConsent
61+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
6162
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: $buyerIdentity) {
6263
cart {
6364
...CartApiMutation

packages/hydrogen/src/cart/queries/cartCreateDefault.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export const CART_CREATE_MUTATION = (
5353
$input: CartInput!
5454
$country: CountryCode = ZZ
5555
$language: LanguageCode
56-
) @inContext(country: $country, language: $language) {
56+
$visitorConsent: VisitorConsent
57+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
5758
cartCreate(input: $input) {
5859
cart {
5960
...CartApiMutation

packages/hydrogen/src/cart/queries/cartDeliveryAddressesAddDefault.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export const CART_DELIVERY_ADDRESSES_ADD_MUTATION = (
6969
$addresses: [CartSelectableAddressInput!]!,
7070
$country: CountryCode = ZZ
7171
$language: LanguageCode
72-
) @inContext(country: $country, language: $language) {
72+
$visitorConsent: VisitorConsent
73+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
7374
cartDeliveryAddressesAdd(addresses: $addresses, cartId: $cartId) {
7475
cart {
7576
...CartApiMutation

packages/hydrogen/src/cart/queries/cartDeliveryAddressesRemoveDefault.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export const CART_DELIVERY_ADDRESSES_REMOVE_MUTATION = (
6565
$addressIds: [ID!]!,
6666
$country: CountryCode = ZZ
6767
$language: LanguageCode
68-
) @inContext(country: $country, language: $language) {
68+
$visitorConsent: VisitorConsent
69+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
6970
cartDeliveryAddressesRemove(addressIds: $addressIds, cartId: $cartId) {
7071
cart {
7172
...CartApiMutation

packages/hydrogen/src/cart/queries/cartDeliveryAddressesUpdateDefault.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export const CART_DELIVERY_ADDRESSES_UPDATE_MUTATION = (
8484
$addresses: [CartSelectableAddressUpdateInput!]!,
8585
$country: CountryCode = ZZ
8686
$language: LanguageCode
87-
) @inContext(country: $country, language: $language) {
87+
$visitorConsent: VisitorConsent
88+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
8889
cartDeliveryAddressesUpdate(addresses: $addresses, cartId: $cartId) {
8990
cart {
9091
...CartApiMutation

packages/hydrogen/src/cart/queries/cartDiscountCodesUpdateDefault.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export const CART_DISCOUNT_CODE_UPDATE_MUTATION = (
4848
$discountCodes: [String!]
4949
$language: LanguageCode
5050
$country: CountryCode
51-
) @inContext(country: $country, language: $language) {
51+
$visitorConsent: VisitorConsent
52+
) @inContext(country: $country, language: $language, visitorConsent: $visitorConsent) {
5253
cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
5354
... @defer {
5455
cart {

0 commit comments

Comments
 (0)