@doswiftly/storefront-sdk 4.7.2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +791 -0
  2. package/README.md +10 -10
  3. package/dist/core/auth/auth-client.d.ts +14 -8
  4. package/dist/core/auth/auth-client.d.ts.map +1 -1
  5. package/dist/core/auth/auth-client.js +26 -20
  6. package/dist/core/auth/handlers.d.ts +31 -0
  7. package/dist/core/auth/handlers.d.ts.map +1 -1
  8. package/dist/core/auth/handlers.js +69 -0
  9. package/dist/core/auth/types.d.ts +5 -5
  10. package/dist/core/auth/types.d.ts.map +1 -1
  11. package/dist/core/cache.d.ts +1 -1
  12. package/dist/core/cache.js +1 -1
  13. package/dist/core/cart/cart-client.d.ts +1 -1
  14. package/dist/core/cart/cart-client.js +1 -1
  15. package/dist/core/cart/types.d.ts +65 -45
  16. package/dist/core/cart/types.d.ts.map +1 -1
  17. package/dist/core/client/compose.d.ts +1 -1
  18. package/dist/core/client/compose.js +1 -1
  19. package/dist/core/client/create-client.d.ts +2 -2
  20. package/dist/core/client/create-client.js +2 -2
  21. package/dist/core/client/execute.d.ts.map +1 -1
  22. package/dist/core/client/execute.js +6 -0
  23. package/dist/core/client/types.js +1 -1
  24. package/dist/core/index.d.ts +2 -2
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +1 -1
  27. package/dist/core/language/cookie-config.d.ts +7 -2
  28. package/dist/core/language/cookie-config.d.ts.map +1 -1
  29. package/dist/core/language/cookie-config.js +7 -2
  30. package/dist/core/operations/auth.d.ts +11 -5
  31. package/dist/core/operations/auth.d.ts.map +1 -1
  32. package/dist/core/operations/auth.js +22 -16
  33. package/dist/core/operations/cart.d.ts +8 -8
  34. package/dist/core/operations/cart.d.ts.map +1 -1
  35. package/dist/core/operations/cart.js +58 -42
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/react/hooks/use-auth.d.ts +4 -4
  39. package/dist/react/hooks/use-auth.d.ts.map +1 -1
  40. package/dist/react/hooks/use-auth.js +11 -18
  41. package/dist/react/hooks/use-cart-manager.d.ts +1 -1
  42. package/dist/react/hooks/use-cart-manager.js +1 -1
  43. package/dist/react/index.d.ts +1 -1
  44. package/dist/react/index.d.ts.map +1 -1
  45. package/dist/react/stores/auth.store.d.ts +0 -2
  46. package/dist/react/stores/auth.store.d.ts.map +1 -1
  47. package/dist/react/stores/auth.store.js +10 -7
  48. package/package.json +2 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,796 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 1efbaab: # Storefront SDK 7.0 — sync z `@doswiftly/storefront-operations@7.0.0`
8
+
9
+ Major bump synchronizujący SDK z schematem GraphQL 7.0. Od tej wersji oba pakiety są publishowane synchronicznie — masz zainstalowaną tę samą wersję obu, niezgodne kombinacje są blokowane.
10
+
11
+ ## Breaking changes
12
+
13
+ ### Cart cost — `Money` zamiast `PriceMoney`, dodany `checkoutCharge`
14
+
15
+ Pola w `CartCost` i `CartLineCost` zwracają teraz `Money` (default scalar — `amount`, `currencyCode`) zamiast `PriceMoney` (currency-conversion transparency: `baseAmount`, `exchangeRate`, `marginApplied`, etc.). Konwersja waluty pozostaje w opt-in `*WithConversion` polach po stronie serwera dla potrzebujących UI'ów (storefront landing page price, hero promo).
16
+
17
+ Plus `CartCost` dostał nowe nullable pole `checkoutCharge: Money` — kwota pre-auth dla bramek płatniczych z separate authorization step.
18
+
19
+ ### Cart lines — Relay Connection (edges/nodes/pageInfo/totalCount)
20
+
21
+ `Cart.lines` zwraca teraz `CartLineConnection` zamiast flat `[CartLine!]`. Klienci czytają `cart.lines.nodes` (preferowane) lub iterują `cart.lines.edges[].node`.
22
+
23
+ ```typescript
24
+ // Przed (5.x)
25
+ for (const line of cart.lines) { ... }
26
+ const count = cart.lines.length;
27
+
28
+ // Po (7.0)
29
+ for (const line of cart.lines.nodes) { ... }
30
+ const count = cart.lines.totalCount;
31
+ ```
32
+
33
+ ### Cart line input/output — `variantId` zamiast `merchandiseId`, `variant` zamiast `merchandise`
34
+
35
+ ```typescript
36
+ // Przed (5.x)
37
+ await cartClient.addItems(cartId, [{ merchandiseId: "var-1", quantity: 1 }]);
38
+ console.log(cart.lines[0].merchandise.title);
39
+
40
+ // Po (7.0)
41
+ await cartClient.addItems(cartId, [{ variantId: "var-1", quantity: 1 }]);
42
+ console.log(cart.lines.nodes[0].variant.title);
43
+ ```
44
+
45
+ ### Cart cost field rename — bezsufiksowo
46
+
47
+ | 5.x | 7.0 |
48
+ | ----------------------------------------- | --------------------------------- |
49
+ | `CartCost.totalAmount` | `total` |
50
+ | `CartCost.subtotalAmount` | `subtotal` |
51
+ | `CartCost.totalTaxAmount` | `totalTax` |
52
+ | `CartCost.totalDutyAmount` | `totalDuty` |
53
+ | (brak) | `checkoutCharge` (nowe, nullable) |
54
+ | `CartLineCost.amountPerQuantity` | `pricePerUnit` |
55
+ | `CartLineCost.subtotalAmount` | `subtotal` |
56
+ | `CartLineCost.totalAmount` | `total` |
57
+ | `CartLineCost.compareAtAmountPerQuantity` | `compareAtPricePerUnit` |
58
+
59
+ ```typescript
60
+ // Przed
61
+ const total = cart.cost.totalAmount.amount;
62
+ const linePrice = cart.lines[0].cost.amountPerQuantity.amount;
63
+
64
+ // Po
65
+ const total = cart.cost.total.amount;
66
+ const linePrice = cart.lines.nodes[0].cost.pricePerUnit.amount;
67
+ ```
68
+
69
+ ### ProductVariant — przebudowane pola
70
+
71
+ | 5.x | 7.0 |
72
+ | ----------------------------------------- | ----------------------------------------------------------------------------- |
73
+ | `available: boolean` | `isAvailable: boolean` |
74
+ | `quantityAvailable: number` | `availableStock: number` |
75
+ | `position: number` | `sortOrder: number` |
76
+ | `weight: number` (scalar) | `weight: { value: number, unit: string }` |
77
+ | `image: Image` (custom shape) | `image: ImageThumbnail` (spread `id, url, altText, width, height, thumbhash`) |
78
+ | `originalPrice`, `originalCompareAtPrice` | (usunięte — opt-in `*WithConversion` jeśli potrzebujesz konwersji) |
79
+
80
+ ### CartLine — dodane `attributeSelections`
81
+
82
+ Customer-filled konfigurator (`AttributeType` z surcharge snapshot — Faza 1 R5). Distinct od `attributes` (raw key/value Line Item Properties).
83
+
84
+ ```typescript
85
+ const line = cart.lines.nodes[0];
86
+ console.log(line.attributes); // [{ key: 'gift_message', value: '...' }]
87
+ console.log(line.attributeSelections); // [{ attributeName, optionLabel, surchargeAmount, ... }]
88
+ ```
89
+
90
+ ### CartDiscountCode — `isApplicable` zamiast `applicable`
91
+
92
+ ```typescript
93
+ // Przed
94
+ if (code.applicable) { ... }
95
+
96
+ // Po
97
+ if (code.isApplicable) { ... }
98
+ ```
99
+
100
+ ### Auth mutations — sync z domeną backend
101
+
102
+ Mutacje GraphQL nazewnictwo zmienione zgodne z naszą domeną:
103
+
104
+ | 5.x mutation | 7.0 mutation |
105
+ | --------------------------- | ---------------------- |
106
+ | `customerAccessTokenCreate` | `customerLogin` |
107
+ | `customerAccessTokenDelete` | `customerLogout` |
108
+ | `customerAccessTokenRenew` | `customerRefreshToken` |
109
+ | `customerCreate` | `customerSignup` |
110
+
111
+ Używając wysokopoziomowego API (`authClient.login()`, `authClient.register()`) — **nie wymaga zmian** w Twoim kodzie. Surowe queries przez `client.mutate(QUERY_STRING)` — wymagają update'u nazw mutacji.
112
+
113
+ ### Method rename — `renewToken` → `refreshToken`
114
+
115
+ ```typescript
116
+ // Przed
117
+ await authClient.renewToken();
118
+ const { renewToken, isRenewingToken } = useAuth();
119
+
120
+ // Po
121
+ await authClient.refreshToken();
122
+ const { refreshToken, isRefreshingToken } = useAuth();
123
+ ```
124
+
125
+ Typ `TokenRenewResult` → `TokenRefreshResult`.
126
+
127
+ ### `AuthClient.getCustomer()` — drop `accessToken` argument
128
+
129
+ Już od 5.x auth context resolved server-side z httpOnly cookie / `Authorization: Bearer`. W 5.x `getCustomer(accessToken)` był legacy override z 4.x — w 7.0 usunięty całkowicie:
130
+
131
+ ```typescript
132
+ // Przed (5.x — działało, ale ignorowało argument)
133
+ const customer = await authClient.getCustomer(accessToken);
134
+
135
+ // Po (7.0)
136
+ const customer = await authClient.getCustomer();
137
+ ```
138
+
139
+ ## Compatibility matrix
140
+
141
+ | SDK | Backend (operations) |
142
+ | --- | -------------------------- |
143
+ | 7.x | 7.x (linked synchroniczne) |
144
+ | 5.x | 4.x, 5.x, 6.x |
145
+ | 4.x | 4.x |
146
+
147
+ Eksporty SDK (`@doswiftly/storefront-sdk`, `/react`, `/react/server`, `/cache`) bez zmian. Provider API (StorefrontProvider, store factories, middleware pipeline) bez zmian.
148
+
149
+ ## Nowe eksporty typów
150
+
151
+ Dodatkowe typy publicznie dostępne dla template'ów które chcą strict typing po cart sub-strukturach:
152
+
153
+ `CartLineEdge`, `CartLineConnection`, `ProductVariant`, `ProductVariantWeight`, `ImageThumbnail`, `PageInfo`, `AttributeSelection`, `CartAttributeSelectionInput`, `SelectedOption`.
154
+
155
+ ## 5.0.0
156
+
157
+ ### Major Changes
158
+
159
+ - d51eb03: Storefront API: cookie-first authentication + comprehensive typing fixes.
160
+
161
+ **⚠️ Breaking changes — migration required**
162
+
163
+ ### Authentication: cookie-first, drop `customerAccessToken` argument
164
+
165
+ The storefront API no longer accepts `customerAccessToken` as a query/mutation argument.
166
+ Authentication context is now resolved per-request from (in priority order):
167
+ 1. **httpOnly cookie `customerAccessToken`** — set automatically by the API after `customerAccessTokenCreate` / `customerCreate` / `customerAccessTokenRenew` / `customerActivateByUrl` / `customerResetByUrl` / `customerReset`. Browser-based storefronts gain XSS-safe auth out of the box.
168
+ 2. **`Authorization: Bearer <token>` header** — for non-browser clients (mobile native, server-to-server, OAuth integrations per RFC 6750).
169
+
170
+ `customerAccessTokenDelete` (logout) clears the cookie via `Set-Cookie: customerAccessToken=; Max-Age=0`.
171
+
172
+ **Affected operations** (drop `customerAccessToken: String!` arg + value pass-through):
173
+ - Queries: `customer`, `customerOrder`, `shipment`, `return`, `returnsByOrder`
174
+ - Mutations: `customerAccessTokenDelete`, `customerAccessTokenRenew`, `customerUpdate`, `customerAddressCreate`, `customerAddressUpdate`, `customerAddressDelete`, `customerDefaultAddressUpdate`, `returnCreate`, `returnCancel`
175
+
176
+ **Migration**:
177
+
178
+ ```graphql
179
+ # Before (4.x)
180
+ query Customer($customerAccessToken: String!) {
181
+ customer(customerAccessToken: $customerAccessToken) { ... }
182
+ }
183
+
184
+ # After (5.x)
185
+ query Customer {
186
+ customer { ... }
187
+ }
188
+ ```
189
+
190
+ For a browser, send the request with `credentials: 'include'` so the httpOnly cookie travels along. For non-browser clients, set `Authorization: Bearer <jwt>` on each request. The `accessToken` field in mutation responses (`customerAccessTokenCreate.customerAccessToken.accessToken`) is still present for non-browser clients that manage their own token storage.
191
+
192
+ ### `LANGUAGE_HEADER_NAME` constant renamed `X-Lang` → `X-Language`
193
+
194
+ Storefronts importing this constant from `@doswiftly/storefront-sdk` will pick up the new value at rebuild time. The previous value did not match the API reader and silently fell through to `Accept-Language` auto-detection — explicit per-request language switching now works as documented.
195
+
196
+ ### `@inContext` directive accepts new arguments
197
+
198
+ `@inContext` previously exposed only `preferredLocationId`. It now also accepts `country: String`, `language: String`, `currency: String`, and `buyer: BuyerInput { customerAccessToken, companyLocationId }`. Resolution priority: `directive > request header > cookie > Accept-Language auto-detect > shop default`. All five layers are independent — pick whichever matches your routing/SSR architecture.
199
+
200
+ ### `Shop.primaryDomain` is now a `Domain` object
201
+
202
+ ```graphql
203
+ # Before
204
+ type Shop {
205
+ primaryDomain: String # "https://example.com"
206
+ primaryDomainObject: Domain! # { host, url, sslEnabled }
207
+ }
208
+
209
+ # After
210
+ type Shop {
211
+ primaryDomain: Domain! # { host, url, sslEnabled }
212
+ }
213
+ ```
214
+
215
+ Update the `Shop` fragment to query the structured fields:
216
+
217
+ ```graphql
218
+ fragment Shop on Shop {
219
+ primaryDomain { host url sslEnabled }
220
+ ...
221
+ }
222
+ ```
223
+
224
+ ### Removed redundant fields
225
+ - `Menu.itemsCount` — drop. Use `items.length` in the client (the count was always equal to the array length).
226
+ - `GiftCard.lastCharacters` — drop. Compute from `maskedCode.slice(-4)` in the client.
227
+
228
+ ### Stronger types — `Date`, enums, structured errors
229
+
230
+ Several fields previously returned `String` (ISO 8601) or untyped error arrays — now properly typed:
231
+ - **DateTime**: `Order.processedAt`, `BlogPost.{publishedAt,createdAt,updatedAt}`, `ShopPage.{publishedAt,createdAt,updatedAt}`, `LoyaltyMember.{lastActivityAt,enrolledAt}`, `LoyaltyTransaction.{createdAt,expiresAt}`, `LoyaltyPointsSummary.nextExpiryDate`, `CustomerAccessToken.expiresAt`. Codegen now emits `Date | string` (or your codegen's date type).
232
+ - **Enums**: `Customer.emailMarketingState` is `EmailMarketingState!`, `ProductAttributeDefinition.fillingMode/billingMode` are typed enums, `ProductAttributeOption.surchargeType` is `AttributeOptionSurchargeType`, `Shop.currencyCode` / `paymentCurrencies` / `supportedCurrencies` use `CurrencyCode`, `Shop.defaultLanguage` / `supportedLanguages` use `LanguageCode`, `Currency.code` is `CurrencyCode`, `Currency.symbolPosition` is the new `CurrencySymbolPosition` enum (`BEFORE` / `AFTER`).
233
+ - **Structured errors**: `WishlistPayload.userErrors`, `RedeemRewardPayload.userErrors`, `GenerateReferralCodePayload.userErrors` are now `[UserError!]!` (`{ message, code, field }`) instead of `[String!]!`. Replace `userErrors[0]` with `userErrors[0].message` in your error handling.
234
+
235
+ ### New: `ProductReview.unhelpfulCount`
236
+
237
+ Storefronts can now render upvote/downvote counts side-by-side without computing one from the other.
238
+
239
+ ### Cross-domain cookie auth — `credentials: 'include'`
240
+
241
+ The SDK GraphQL transport now sends `credentials: 'include'` on every request. If your storefront runs on a domain different from the API and uses cookie auth, the API CORS configuration must allow your origin (with `Access-Control-Allow-Credentials: true`). Same-origin deployments are unaffected.
242
+
243
+ ### Auth store no longer persists `accessToken` to localStorage (XSS hardening)
244
+
245
+ `useAuthStore` only persists `customer` and `isAuthenticated` to localStorage. The token lives in the httpOnly cookie (browser auto-sent) plus in-memory store state (set by login flow). After a page refresh, browser-based storefronts call `GET /api/auth/whoami` (new BFF route shipped with the template) to rehydrate `customer` info — the cookie continues to authenticate every subsequent GraphQL request transparently.
246
+
247
+ The `auth-storage` localStorage version bumps from `v2` to `v3`; the migrate handler clears legacy persisted tokens on first load.
248
+
249
+ ### New: `createWhoamiHandler()` factory
250
+
251
+ Exports from `@doswiftly/storefront-sdk`. Pair with the `app/api/auth/whoami/route.ts` example shipped with the CLI template:
252
+
253
+ ```typescript
254
+ // app/api/auth/whoami/route.ts
255
+ import { createWhoamiHandler } from "@doswiftly/storefront-sdk";
256
+ export const GET = createWhoamiHandler({
257
+ apiUrl: process.env.NEXT_PUBLIC_API_URL,
258
+ shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG,
259
+ });
260
+ ```
261
+
262
+ ### `AuthClient` (low-level core API) — drop token argument
263
+
264
+ ```typescript
265
+ // Before
266
+ authClient.logout(token);
267
+ authClient.renewToken(token);
268
+ authClient.getCustomer(token);
269
+
270
+ // After
271
+ authClient.logout(); // auth via cookie/Bearer in middleware
272
+ authClient.renewToken();
273
+ authClient.getCustomer();
274
+ ```
275
+
276
+ ### Template (`@doswiftly/cli` scaffolded storefront)
277
+
278
+ The Next.js template now uses cookie-first auth end-to-end:
279
+ - `lib/graphql/hooks.ts` — query/mutation hooks no longer pass `customerAccessToken`
280
+ - `lib/graphql/server.ts` — server-side helper reads `customerAccessToken` cookie via `next/headers` and forwards as `Authorization: Bearer`
281
+ - `app/api/auth/whoami/route.ts` (new) — BFF route for `customer` rehydration after page refresh
282
+ - `hooks/use-auth-sync.ts` — rewritten to use the whoami endpoint instead of token desync detection
283
+ - Auth-gated React components/pages now check `isAuthenticated` instead of `accessToken`
284
+
285
+ ### Wishlist pagination + GiftCard read surface
286
+ - **`wishlists`** is now a paginated **`WishlistConnection`** (Relay pattern). Replace direct array selection with `nodes` (shortcut) or `edges { cursor node { ... } }`. Supports `first: Int = 20` and `after: String` cursor arguments.
287
+
288
+ ```graphql
289
+ # Before (4.x)
290
+ query Wishlists {
291
+ wishlists {
292
+ id
293
+ name
294
+ itemCount
295
+ items {
296
+ id
297
+ productId
298
+ }
299
+ }
300
+ }
301
+
302
+ # After (5.x)
303
+ query Wishlists($first: Int = 20, $after: String) {
304
+ wishlists(first: $first, after: $after) {
305
+ nodes {
306
+ id
307
+ name
308
+ itemCount
309
+ items {
310
+ id
311
+ productId
312
+ }
313
+ }
314
+ pageInfo {
315
+ hasNextPage
316
+ hasPreviousPage
317
+ startCursor
318
+ endCursor
319
+ }
320
+ totalCount
321
+ }
322
+ }
323
+ ```
324
+
325
+ - **`giftCard(code)`** read query no longer wraps result in `GiftCardPayload`. Returns nullable `GiftCard` directly (`null` = not found). The wrapper pattern `{ giftCard, userErrors }` is reserved for mutations only.
326
+
327
+ ```graphql
328
+ # Before (4.x)
329
+ query GiftCard($code: String!) {
330
+ giftCard(code: $code) {
331
+ giftCard {
332
+ id
333
+ maskedCode
334
+ balance {
335
+ amount
336
+ currencyCode
337
+ }
338
+ }
339
+ userErrors {
340
+ message
341
+ code
342
+ }
343
+ }
344
+ }
345
+
346
+ # After (5.x)
347
+ query GiftCard($code: String!) {
348
+ giftCard(code: $code) {
349
+ id
350
+ maskedCode
351
+ balance {
352
+ amount
353
+ currencyCode
354
+ }
355
+ }
356
+ }
357
+ ```
358
+
359
+ - **`GiftCard.transactions`** field removed from storefront API entirely. The field always returned `[]` (PII safety: transactions contain `orderId` / `customerId` not safe to expose at anonymous code-lookup endpoint). Schema now rejects the selection at validation time. Storefronts that selected this field will get a clear schema error instead of an always-empty array.
360
+
361
+ ### Filter scalars + structured Weight type
362
+ - **`Filter.id` and `FilterValue.id`** are now `ID!` (previously `String!`). Codegen treats `ID` identically to `String` in TypeScript, but downstream client tooling (Relay-style caches, normalized stores) gains proper identity semantics for entity deduplication.
363
+ - **`FilterValue.input`** changed from `String!` (JSON-stringified payload) to **`JSON!`** (new opaque structured scalar). Storefronts no longer need to `JSON.parse(filterValue.input)` before passing it to the `filters` argument — the value arrives as a structured object ready to use:
364
+
365
+ ```graphql
366
+ # Before (4.x) — client-side parsing required
367
+ filterValue.input # "{\"variantOption\":{\"name\":\"Color\",\"value\":\"Red\"}}"
368
+ JSON.parse(filterValue.input)
369
+
370
+ # After (5.x) — directly passable as filters arg element
371
+ filterValue.input # { variantOption: { name: "Color", value: "Red" } }
372
+ ```
373
+
374
+ - **`ProductVariant.weight`** changed from `Float` (raw grams, implicit unit per docs) to **`Weight { value: Float!, unit: WeightUnit! }`** structured type with the new `WeightUnit` enum (`GRAMS | KILOGRAMS | OUNCES | POUNDS`). The API always returns `unit: GRAMS` (canonical storage unit). Storefronts can convert to the preferred unit for shipping or checkout UI without hardcoding "grams" anywhere in the codebase:
375
+
376
+ ```graphql
377
+ # Before (4.x)
378
+ variant { weight } # 250 (Float, "grams" implicit per docs)
379
+
380
+ # After (5.x)
381
+ variant { weight { value unit } } # { value: 250, unit: GRAMS }
382
+ ```
383
+
384
+ ### Removed: legacy `x-customer-access-token` header
385
+
386
+ The legacy `x-customer-access-token` header reader (which never had a matching SDK middleware) is removed from the API. If a custom integration was relying on it, switch to `Authorization: Bearer <token>`.
387
+
388
+ ### Field & mutation naming sweep
389
+
390
+ Several renames have landed in this release. Update queries and variables accordingly:
391
+ - **Cart mutations**: `cartLinesAdd` → `cartAddLines`, `cartLinesUpdate` → `cartUpdateLines`, `cartLinesRemove` → `cartRemoveLines`, `cartNoteUpdate` → `cartUpdateNote`, `cartAttributesUpdate` → `cartUpdateAttributes`, `cartBuyerIdentityUpdate` → `cartUpdateBuyerIdentity`, `cartDiscountCodesUpdate` → `cartApplyDiscountCodes`. Resource-id arg renamed `cartId` → `id` on mutations (the foreign-key `CheckoutCreateInput.cartId` keeps its name).
392
+ - **Checkout mutations**: `checkoutId` → `id`. `checkoutSelectShippingRate` arg `shippingRateHandle` → `rateId`.
393
+ - **Wishlist mutations**: `wishlistId` → `id`.
394
+ - **Field renames on output types**:
395
+ - `Product.productType` → `Product.category` (free-text classification field; the `Product.type: ProductTypeEnum` enum field is unchanged)
396
+ - `ProductVariant.quantityAvailable` → `availableStock` (disambiguates effective availability from raw inventory)
397
+ - `ProductOption.position` / `ProductOptionValue.position` → `sortOrder`
398
+ - `Customer.numberOfOrders` → `orderCount`
399
+ - `WishlistItem.priceAtAdd` → `priceWhenAdded`
400
+ - `Shop.primaryDomain.sslEnabled` → `isSslEnabled` (boolean prefix convention)
401
+ - `Shipment.estimatedDeliveryDate` → `estimatedDeliveryAt`
402
+ - `StoreAvailability.pickUpTime` → `pickupTime`
403
+ - `Location.pickupEnabled` → `supportsPickup`
404
+ - `ShopPage.bodySummary` → `excerpt`
405
+ - **New field**: `ProductReview.unhelpfulCount` — render upvote/downvote counts side-by-side without computing one from the other.
406
+ - **`UserError`/payload selection set** is now required on `WishlistPayload.userErrors` (and other payloads switched to typed `[UserError!]!`). Bare `userErrors` selection without a sub-selection is rejected by schema validation.
407
+ - **Error code stability**: typed error codes (e.g. `CART_NOT_FOUND`, `CUSTOMER_TOKEN_INVALID`) are now locale-agnostic — they remain stable across language responses (in earlier versions the mapping silently regressed when a translated message lost a specific English keyword).
408
+
409
+ ### Unified `UserError` across all mutation payloads
410
+
411
+ Per-domain user-error types (`CartUserError`, `CheckoutUserError`, `CustomerUserError`) have been replaced by the single generic `UserError` type used everywhere. Error codes remain unique per domain (e.g. `CART_NOT_FOUND` vs `CUSTOMER_NOT_FOUND` vs `CHECKOUT_PAYMENT_FAILED`) — string comparison on the `code` field gives the same branching power without per-domain type proliferation.
412
+
413
+ Customer mutation payloads also rename the error field from `customerUserErrors` to `userErrors` for consistency with cart, checkout, wishlist, and other payloads:
414
+
415
+ ```graphql
416
+ # Before (4.x)
417
+ mutation CartCreate {
418
+ cartCreate {
419
+ userErrors {
420
+ ...CartUserError
421
+ }
422
+ }
423
+ }
424
+ mutation CustomerLogin {
425
+ customerLogin {
426
+ customerUserErrors {
427
+ ...CustomerUserError
428
+ }
429
+ }
430
+ }
431
+ mutation CheckoutCreate {
432
+ checkoutCreate {
433
+ userErrors {
434
+ ...CheckoutUserError
435
+ }
436
+ }
437
+ }
438
+
439
+ # After (5.x)
440
+ mutation CartCreate {
441
+ cartCreate {
442
+ userErrors {
443
+ ...UserError
444
+ }
445
+ }
446
+ }
447
+ mutation CustomerLogin {
448
+ customerLogin {
449
+ userErrors {
450
+ ...UserError
451
+ }
452
+ }
453
+ }
454
+ mutation CheckoutCreate {
455
+ checkoutCreate {
456
+ userErrors {
457
+ ...UserError
458
+ }
459
+ }
460
+ }
461
+ ```
462
+
463
+ Update the shared `UserError` fragment as the only error fragment you need:
464
+
465
+ ```graphql
466
+ fragment UserError on UserError {
467
+ message
468
+ code # namespaced string — e.g. "CART_NOT_FOUND", "CUSTOMER_TOKEN_INVALID"
469
+ field # path to the input field that caused the error
470
+ }
471
+ ```
472
+
473
+ Client TypeScript change after codegen: `userErrors[i].code` is now `string | null` instead of a strongly-typed enum (`CartErrorCode` / `CheckoutErrorCode` / `CustomerErrorCode`). Switch from `code === CartErrorCode.CART_NOT_FOUND` to `code === 'CART_NOT_FOUND'`. The full list of namespaced codes per domain is documented in the schema descriptions on each payload's `userErrors` field.
474
+
475
+ ### Address `country` field — `CountryCode` enum on inputs
476
+
477
+ Address input types (`MailingAddressInput`, `CheckoutAddressInput`, `ShippingAddressInput`) now use the `CountryCode` enum for the `country` field instead of `String` with regex validation. This matches the pattern already used by output types (`MailingAddress.countryCode: CountryCode`) and gives client codegen a strongly-typed enum value instead of a free-form string:
478
+
479
+ ```graphql
480
+ # Before (4.x)
481
+ input CheckoutAddressInput {
482
+ country: String! # validated server-side via @Matches(/^[A-Z]{2}$/)
483
+ }
484
+
485
+ # After (5.x)
486
+ input CheckoutAddressInput {
487
+ country: CountryCode! # GraphQL enum — invalid values rejected at validation
488
+ }
489
+ ```
490
+
491
+ Output types keep two distinct fields where applicable: `country` (display name, e.g. "Polska") + `countryCode` (typed enum, e.g. `PL`).
492
+
493
+ ### Address fields — international naming
494
+
495
+ `MailingAddress`, `LocationAddress`, `ShopAddress`, and the address inputs (`MailingAddressInput`, `CheckoutAddressInput`) now use international neutral terminology instead of US/Canadian-centric field names. The new names match common international postal conventions and the underlying database column names:
496
+
497
+ | Before (4.x) | After (5.x) |
498
+ | -------------- | ------------- |
499
+ | `address1` | `streetLine1` |
500
+ | `address2` | `streetLine2` |
501
+ | `province` | `state` |
502
+ | `provinceCode` | `stateCode` |
503
+ | `zip` | `postalCode` |
504
+
505
+ ```graphql
506
+ # Before (4.x)
507
+ fragment MailingAddress on MailingAddress {
508
+ address1
509
+ address2
510
+ province
511
+ zip
512
+ }
513
+
514
+ # After (5.x)
515
+ fragment MailingAddress on MailingAddress {
516
+ streetLine1
517
+ streetLine2
518
+ state
519
+ postalCode
520
+ }
521
+
522
+ # Mutation input
523
+ mutation AddAddress($input: MailingAddressInput!) {
524
+ customerAddAddress(input: $input) { ... }
525
+ }
526
+ # variables (4.x): { address1: "...", province: "...", zip: "..." }
527
+ # variables (5.x): { streetLine1: "...", state: "...", postalCode: "..." }
528
+ ```
529
+
530
+ **Backward-compat for existing orders**: `Order.shippingAddress` and `Order.billingAddress` are stored as JSONB snapshots at order placement time. Snapshots created under the previous API contain `address1` / `province` / `zip`; new snapshots use `streetLine1` / `state` / `postalCode`. The `MailingAddress` resolver reads new field names first and falls back to legacy names — historical orders continue to render correctly. Frontend codegen will emit the new field names; templates rendering historical orders gain the new schema without data migration. Backward-compat fallback is scheduled for removal once historical snapshots reach end-of-life (~2-3 years).
531
+
532
+ **`MailingAddress.formatted`** (country-aware ordered lines) continues to work — internal helper updated to use new field names. Storefronts rendering addresses via `formatted` need no changes.
533
+
534
+ ### Costs and totals — nested wrappers everywhere
535
+
536
+ Cart, Checkout, and Order now expose monetary breakdowns via dedicated wrapper types instead of flat fields on the parent. This adds a single hop in selections but makes adding new cost dimensions (duty, fees, surcharges) non-breaking, and brings symmetry across the three checkout-flow stages:
537
+
538
+ ```graphql
539
+ # CartCost — drop "Amount" suffix from field names (Money type implies amount)
540
+ fragment CartCost on CartCost {
541
+ subtotal {
542
+ ...Money
543
+ } # was: subtotalAmount
544
+ total {
545
+ ...Money
546
+ } # was: totalAmount
547
+ totalTax {
548
+ ...Money
549
+ } # was: totalTaxAmount
550
+ totalDuty {
551
+ ...Money
552
+ } # was: totalDutyAmount
553
+ checkoutCharge {
554
+ ...Money
555
+ } # was: checkoutChargeAmount
556
+ }
557
+
558
+ # CartLineCost — drop "Amount" suffix + "amountPerQuantity" → "pricePerUnit"
559
+ fragment CartLineCost on CartLineCost {
560
+ pricePerUnit {
561
+ ...Money
562
+ } # was: amountPerQuantity
563
+ subtotal {
564
+ ...Money
565
+ } # was: subtotalAmount
566
+ total {
567
+ ...Money
568
+ } # was: totalAmount
569
+ compareAtPricePerUnit {
570
+ ...Money
571
+ } # was: compareAtAmountPerQuantity
572
+ }
573
+
574
+ # CheckoutCost — NEW wrapper, replaces flat checkout fields
575
+ query Checkout {
576
+ checkout {
577
+ cost {
578
+ subtotal {
579
+ ...Money
580
+ } # was: Checkout.subtotalPrice
581
+ total {
582
+ ...Money
583
+ } # was: Checkout.totalPrice
584
+ totalTax {
585
+ ...Money
586
+ } # was: Checkout.totalTax (now nested)
587
+ totalShipping {
588
+ ...Money
589
+ } # was: Checkout.totalShippingPrice
590
+ totalDiscounts {
591
+ ...Money
592
+ } # was: Checkout.totalDiscounts (now nested)
593
+ }
594
+ }
595
+ }
596
+
597
+ # OrderTotals — NEW wrapper, replaces flat order fields
598
+ query CustomerOrder {
599
+ customerOrder {
600
+ totals {
601
+ subtotal {
602
+ ...Money
603
+ } # was: Order.subtotalPrice
604
+ total {
605
+ ...Money
606
+ } # was: Order.totalPrice
607
+ totalTax {
608
+ ...Money
609
+ } # was: Order.totalTax (now nested)
610
+ totalShipping {
611
+ ...Money
612
+ } # was: Order.totalShipping (now nested)
613
+ }
614
+ }
615
+ }
616
+
617
+ # CheckoutLineItem — line price field renames
618
+ fragment CheckoutLineItem on CheckoutLineItem {
619
+ pricePerUnit {
620
+ ...Money
621
+ } # was: unitPrice
622
+ total {
623
+ ...Money
624
+ } # was: totalPrice
625
+ }
626
+ ```
627
+
628
+ Conversion-transparency opt-in fields (`*WithConversion`) are renamed in lockstep:
629
+ - `subtotalAmountWithConversion` → `subtotalWithConversion`
630
+ - `totalAmountWithConversion` → `totalWithConversion`
631
+ - `amountPerQuantityWithConversion` → `pricePerUnitWithConversion`
632
+ - ...and so on.
633
+
634
+ ### Cart line: simplified shape — `variant` replaces `merchandise` union
635
+
636
+ `CartLine.merchandise: Merchandise!` (a union currently with only `ProductVariant` member) and the `BaseCartLine` interface have been removed. `CartLine` is now a concrete type with a direct `variant: ProductVariant!` field — no union wrapper, no inline fragments needed:
637
+
638
+ ```graphql
639
+ # Before (4.x)
640
+ fragment CartLine on CartLine {
641
+ id
642
+ quantity
643
+ merchandise {
644
+ ... on ProductVariant {
645
+ id
646
+ title
647
+ price { amount currencyCode }
648
+ }
649
+ }
650
+ cost { ... }
651
+ }
652
+
653
+ # After (5.x) — direct field, no union/inline fragment
654
+ fragment CartLine on CartLine {
655
+ id
656
+ quantity
657
+ variant {
658
+ id
659
+ title
660
+ price { amount currencyCode }
661
+ }
662
+ cost { ... }
663
+ }
664
+ ```
665
+
666
+ Cart connection types renamed for the same reason: `BaseCartLineConnection` / `BaseCartLineEdge` → `CartLineConnection` / `CartLineEdge`. Update fragment selections accordingly:
667
+
668
+ ```graphql
669
+ # Before (4.x)
670
+ query Cart($id: ID!) {
671
+ cart(id: $id) {
672
+ lines(first: 10) {
673
+ edges {
674
+ node {
675
+ ... on CartLine {
676
+ id
677
+ }
678
+ }
679
+ }
680
+ nodes {
681
+ ... on CartLine {
682
+ id
683
+ }
684
+ }
685
+ }
686
+ }
687
+ }
688
+
689
+ # After (5.x)
690
+ query Cart($id: ID!) {
691
+ cart(id: $id) {
692
+ lines(first: 10) {
693
+ edges {
694
+ node {
695
+ id
696
+ }
697
+ }
698
+ nodes {
699
+ id
700
+ }
701
+ }
702
+ }
703
+ }
704
+ ```
705
+
706
+ Cart line input renamed `merchandiseId` → `variantId` (consistent with the output field):
707
+
708
+ ```graphql
709
+ # Before (4.x)
710
+ mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
711
+ cartAddLines(id: $id, lines: $lines) { ... }
712
+ }
713
+ # variables: { lines: [{ merchandiseId: "uuid", quantity: 1 }] }
714
+
715
+ # After (5.x)
716
+ mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
717
+ cartAddLines(id: $id, lines: $lines) { ... }
718
+ }
719
+ # variables: { lines: [{ variantId: "uuid", quantity: 1 }] }
720
+ ```
721
+
722
+ The `variantId` name also matches the `userErrors[].field` path returned for input-related errors (e.g. `field: ["lines", "variantId"]`).
723
+
724
+ ### Recommendations: top-level queries replaced by field resolvers
725
+
726
+ Two top-level queries were removed. Use the field resolvers on the parent entity instead — they return identical payloads with automatic context inheritance, cleaner cache invalidation, and a clearer parent-not-found vs empty-recommendations distinction:
727
+
728
+ ```graphql
729
+ # Before (4.x)
730
+ query SimilarProducts($productId: String!, $first: Int) {
731
+ similarProducts(productId: $productId, first: $first) {
732
+ items {
733
+ product {
734
+ id
735
+ title
736
+ }
737
+ type
738
+ score
739
+ }
740
+ totalCount
741
+ }
742
+ }
743
+ query CartRecommendations($cartId: String!, $first: Int) {
744
+ cartRecommendations(cartId: $cartId, first: $first) {
745
+ frequentlyBoughtTogether {
746
+ product {
747
+ id
748
+ }
749
+ }
750
+ youMayAlsoLike {
751
+ product {
752
+ id
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ # After (5.x) — same payloads, parent-scoped
759
+ query Product($id: ID!, $first: Int) {
760
+ product(id: $id) {
761
+ recommendations(first: $first) {
762
+ items {
763
+ product {
764
+ id
765
+ title
766
+ }
767
+ type
768
+ score
769
+ }
770
+ totalCount
771
+ }
772
+ }
773
+ }
774
+ query Cart($id: ID!, $first: Int) {
775
+ cart(id: $id) {
776
+ recommendations(first: $first) {
777
+ frequentlyBoughtTogether {
778
+ product {
779
+ id
780
+ }
781
+ }
782
+ youMayAlsoLike {
783
+ product {
784
+ id
785
+ }
786
+ }
787
+ }
788
+ }
789
+ }
790
+ ```
791
+
792
+ For broader product-scoped recommendations driven by intent (`SIMILAR | COMPLEMENTARY | UPSELL`), use the existing top-level `productRecommendations(productId, intent, limit)` query, which returns a flat `[Product!]!` payload — separate surface from the rich `Product.recommendations` field (which carries metadata `type`, `score`, `reason` per item).
793
+
3
794
  ## 4.7.2
4
795
 
5
796
  ### Patch Changes