@doswiftly/storefront-sdk 4.7.1 → 5.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 (40) hide show
  1. package/CHANGELOG.md +788 -0
  2. package/dist/core/auth/auth-client.d.ts +15 -8
  3. package/dist/core/auth/auth-client.d.ts.map +1 -1
  4. package/dist/core/auth/auth-client.js +18 -11
  5. package/dist/core/auth/handlers.d.ts +31 -0
  6. package/dist/core/auth/handlers.d.ts.map +1 -1
  7. package/dist/core/auth/handlers.js +69 -0
  8. package/dist/core/auth/types.d.ts +5 -5
  9. package/dist/core/auth/types.d.ts.map +1 -1
  10. package/dist/core/cache.d.ts +1 -1
  11. package/dist/core/cache.js +1 -1
  12. package/dist/core/cart/types.d.ts +1 -1
  13. package/dist/core/cart/types.d.ts.map +1 -1
  14. package/dist/core/client/compose.d.ts +1 -1
  15. package/dist/core/client/compose.js +1 -1
  16. package/dist/core/client/create-client.d.ts +2 -2
  17. package/dist/core/client/create-client.js +2 -2
  18. package/dist/core/client/execute.d.ts.map +1 -1
  19. package/dist/core/client/execute.js +6 -0
  20. package/dist/core/client/types.js +1 -1
  21. package/dist/core/index.d.ts +1 -1
  22. package/dist/core/index.d.ts.map +1 -1
  23. package/dist/core/index.js +1 -1
  24. package/dist/core/language/cookie-config.d.ts +7 -2
  25. package/dist/core/language/cookie-config.d.ts.map +1 -1
  26. package/dist/core/language/cookie-config.js +7 -2
  27. package/dist/core/operations/auth.d.ts +4 -4
  28. package/dist/core/operations/auth.d.ts.map +1 -1
  29. package/dist/core/operations/auth.js +11 -11
  30. package/dist/core/operations/cart.d.ts +8 -8
  31. package/dist/core/operations/cart.d.ts.map +1 -1
  32. package/dist/core/operations/cart.js +9 -9
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/react/hooks/use-auth.d.ts.map +1 -1
  36. package/dist/react/hooks/use-auth.js +6 -13
  37. package/dist/react/stores/auth.store.d.ts +0 -2
  38. package/dist/react/stores/auth.store.d.ts.map +1 -1
  39. package/dist/react/stores/auth.store.js +10 -7
  40. package/package.json +4 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,788 @@
1
+ # Changelog
2
+
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - d51eb03: Storefront API: cookie-first authentication + comprehensive typing fixes.
8
+
9
+ **⚠️ Breaking changes — migration required**
10
+
11
+ ### Authentication: cookie-first, drop `customerAccessToken` argument
12
+
13
+ The storefront API no longer accepts `customerAccessToken` as a query/mutation argument.
14
+ Authentication context is now resolved per-request from (in priority order):
15
+ 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.
16
+ 2. **`Authorization: Bearer <token>` header** — for non-browser clients (mobile native, server-to-server, OAuth integrations per RFC 6750).
17
+
18
+ `customerAccessTokenDelete` (logout) clears the cookie via `Set-Cookie: customerAccessToken=; Max-Age=0`.
19
+
20
+ **Affected operations** (drop `customerAccessToken: String!` arg + value pass-through):
21
+ - Queries: `customer`, `customerOrder`, `shipment`, `return`, `returnsByOrder`
22
+ - Mutations: `customerAccessTokenDelete`, `customerAccessTokenRenew`, `customerUpdate`, `customerAddressCreate`, `customerAddressUpdate`, `customerAddressDelete`, `customerDefaultAddressUpdate`, `returnCreate`, `returnCancel`
23
+
24
+ **Migration**:
25
+
26
+ ```graphql
27
+ # Before (4.x)
28
+ query Customer($customerAccessToken: String!) {
29
+ customer(customerAccessToken: $customerAccessToken) { ... }
30
+ }
31
+
32
+ # After (5.x)
33
+ query Customer {
34
+ customer { ... }
35
+ }
36
+ ```
37
+
38
+ 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.
39
+
40
+ ### `LANGUAGE_HEADER_NAME` constant renamed `X-Lang` → `X-Language`
41
+
42
+ 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.
43
+
44
+ ### `@inContext` directive accepts new arguments
45
+
46
+ `@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.
47
+
48
+ ### `Shop.primaryDomain` is now a `Domain` object
49
+
50
+ ```graphql
51
+ # Before
52
+ type Shop {
53
+ primaryDomain: String # "https://example.com"
54
+ primaryDomainObject: Domain! # { host, url, sslEnabled }
55
+ }
56
+
57
+ # After
58
+ type Shop {
59
+ primaryDomain: Domain! # { host, url, sslEnabled }
60
+ }
61
+ ```
62
+
63
+ Update the `Shop` fragment to query the structured fields:
64
+
65
+ ```graphql
66
+ fragment Shop on Shop {
67
+ primaryDomain { host url sslEnabled }
68
+ ...
69
+ }
70
+ ```
71
+
72
+ ### Removed redundant fields
73
+ - `Menu.itemsCount` — drop. Use `items.length` in the client (the count was always equal to the array length).
74
+ - `GiftCard.lastCharacters` — drop. Compute from `maskedCode.slice(-4)` in the client.
75
+
76
+ ### Stronger types — `Date`, enums, structured errors
77
+
78
+ Several fields previously returned `String` (ISO 8601) or untyped error arrays — now properly typed:
79
+ - **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).
80
+ - **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`).
81
+ - **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.
82
+
83
+ ### New: `ProductReview.unhelpfulCount`
84
+
85
+ Storefronts can now render upvote/downvote counts side-by-side without computing one from the other.
86
+
87
+ ### Cross-domain cookie auth — `credentials: 'include'`
88
+
89
+ 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.
90
+
91
+ ### Auth store no longer persists `accessToken` to localStorage (XSS hardening)
92
+
93
+ `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.
94
+
95
+ The `auth-storage` localStorage version bumps from `v2` to `v3`; the migrate handler clears legacy persisted tokens on first load.
96
+
97
+ ### New: `createWhoamiHandler()` factory
98
+
99
+ Exports from `@doswiftly/storefront-sdk`. Pair with the `app/api/auth/whoami/route.ts` example shipped with the CLI template:
100
+
101
+ ```typescript
102
+ // app/api/auth/whoami/route.ts
103
+ import { createWhoamiHandler } from "@doswiftly/storefront-sdk";
104
+ export const GET = createWhoamiHandler({
105
+ apiUrl: process.env.NEXT_PUBLIC_API_URL,
106
+ shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG,
107
+ });
108
+ ```
109
+
110
+ ### `AuthClient` (low-level core API) — drop token argument
111
+
112
+ ```typescript
113
+ // Before
114
+ authClient.logout(token);
115
+ authClient.renewToken(token);
116
+ authClient.getCustomer(token);
117
+
118
+ // After
119
+ authClient.logout(); // auth via cookie/Bearer in middleware
120
+ authClient.renewToken();
121
+ authClient.getCustomer();
122
+ ```
123
+
124
+ ### Template (`@doswiftly/cli` scaffolded storefront)
125
+
126
+ The Next.js template now uses cookie-first auth end-to-end:
127
+ - `lib/graphql/hooks.ts` — query/mutation hooks no longer pass `customerAccessToken`
128
+ - `lib/graphql/server.ts` — server-side helper reads `customerAccessToken` cookie via `next/headers` and forwards as `Authorization: Bearer`
129
+ - `app/api/auth/whoami/route.ts` (new) — BFF route for `customer` rehydration after page refresh
130
+ - `hooks/use-auth-sync.ts` — rewritten to use the whoami endpoint instead of token desync detection
131
+ - Auth-gated React components/pages now check `isAuthenticated` instead of `accessToken`
132
+
133
+ ### Wishlist pagination + GiftCard read surface
134
+ - **`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.
135
+
136
+ ```graphql
137
+ # Before (4.x)
138
+ query Wishlists {
139
+ wishlists {
140
+ id
141
+ name
142
+ itemCount
143
+ items {
144
+ id
145
+ productId
146
+ }
147
+ }
148
+ }
149
+
150
+ # After (5.x)
151
+ query Wishlists($first: Int = 20, $after: String) {
152
+ wishlists(first: $first, after: $after) {
153
+ nodes {
154
+ id
155
+ name
156
+ itemCount
157
+ items {
158
+ id
159
+ productId
160
+ }
161
+ }
162
+ pageInfo {
163
+ hasNextPage
164
+ hasPreviousPage
165
+ startCursor
166
+ endCursor
167
+ }
168
+ totalCount
169
+ }
170
+ }
171
+ ```
172
+
173
+ - **`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.
174
+
175
+ ```graphql
176
+ # Before (4.x)
177
+ query GiftCard($code: String!) {
178
+ giftCard(code: $code) {
179
+ giftCard {
180
+ id
181
+ maskedCode
182
+ balance {
183
+ amount
184
+ currencyCode
185
+ }
186
+ }
187
+ userErrors {
188
+ message
189
+ code
190
+ }
191
+ }
192
+ }
193
+
194
+ # After (5.x)
195
+ query GiftCard($code: String!) {
196
+ giftCard(code: $code) {
197
+ id
198
+ maskedCode
199
+ balance {
200
+ amount
201
+ currencyCode
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ - **`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.
208
+
209
+ ### Filter scalars + structured Weight type
210
+ - **`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.
211
+ - **`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:
212
+
213
+ ```graphql
214
+ # Before (4.x) — client-side parsing required
215
+ filterValue.input # "{\"variantOption\":{\"name\":\"Color\",\"value\":\"Red\"}}"
216
+ JSON.parse(filterValue.input)
217
+
218
+ # After (5.x) — directly passable as filters arg element
219
+ filterValue.input # { variantOption: { name: "Color", value: "Red" } }
220
+ ```
221
+
222
+ - **`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:
223
+
224
+ ```graphql
225
+ # Before (4.x)
226
+ variant { weight } # 250 (Float, "grams" implicit per docs)
227
+
228
+ # After (5.x)
229
+ variant { weight { value unit } } # { value: 250, unit: GRAMS }
230
+ ```
231
+
232
+ ### Removed: legacy `x-customer-access-token` header
233
+
234
+ 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>`.
235
+
236
+ ### Field & mutation naming sweep
237
+
238
+ Several renames have landed in this release. Update queries and variables accordingly:
239
+ - **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).
240
+ - **Checkout mutations**: `checkoutId` → `id`. `checkoutSelectShippingRate` arg `shippingRateHandle` → `rateId`.
241
+ - **Wishlist mutations**: `wishlistId` → `id`.
242
+ - **Field renames on output types**:
243
+ - `Product.productType` → `Product.category` (free-text classification field; the `Product.type: ProductTypeEnum` enum field is unchanged)
244
+ - `ProductVariant.quantityAvailable` → `availableStock` (disambiguates effective availability from raw inventory)
245
+ - `ProductOption.position` / `ProductOptionValue.position` → `sortOrder`
246
+ - `Customer.numberOfOrders` → `orderCount`
247
+ - `WishlistItem.priceAtAdd` → `priceWhenAdded`
248
+ - `Shop.primaryDomain.sslEnabled` → `isSslEnabled` (boolean prefix convention)
249
+ - `Shipment.estimatedDeliveryDate` → `estimatedDeliveryAt`
250
+ - `StoreAvailability.pickUpTime` → `pickupTime`
251
+ - `Location.pickupEnabled` → `supportsPickup`
252
+ - `ShopPage.bodySummary` → `excerpt`
253
+ - **New field**: `ProductReview.unhelpfulCount` — render upvote/downvote counts side-by-side without computing one from the other.
254
+ - **`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.
255
+ - **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).
256
+
257
+ ### Unified `UserError` across all mutation payloads
258
+
259
+ 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.
260
+
261
+ Customer mutation payloads also rename the error field from `customerUserErrors` to `userErrors` for consistency with cart, checkout, wishlist, and other payloads:
262
+
263
+ ```graphql
264
+ # Before (4.x)
265
+ mutation CartCreate {
266
+ cartCreate {
267
+ userErrors {
268
+ ...CartUserError
269
+ }
270
+ }
271
+ }
272
+ mutation CustomerLogin {
273
+ customerLogin {
274
+ customerUserErrors {
275
+ ...CustomerUserError
276
+ }
277
+ }
278
+ }
279
+ mutation CheckoutCreate {
280
+ checkoutCreate {
281
+ userErrors {
282
+ ...CheckoutUserError
283
+ }
284
+ }
285
+ }
286
+
287
+ # After (5.x)
288
+ mutation CartCreate {
289
+ cartCreate {
290
+ userErrors {
291
+ ...UserError
292
+ }
293
+ }
294
+ }
295
+ mutation CustomerLogin {
296
+ customerLogin {
297
+ userErrors {
298
+ ...UserError
299
+ }
300
+ }
301
+ }
302
+ mutation CheckoutCreate {
303
+ checkoutCreate {
304
+ userErrors {
305
+ ...UserError
306
+ }
307
+ }
308
+ }
309
+ ```
310
+
311
+ Update the shared `UserError` fragment as the only error fragment you need:
312
+
313
+ ```graphql
314
+ fragment UserError on UserError {
315
+ message
316
+ code # namespaced string — e.g. "CART_NOT_FOUND", "CUSTOMER_TOKEN_INVALID"
317
+ field # path to the input field that caused the error
318
+ }
319
+ ```
320
+
321
+ 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.
322
+
323
+ ### Address `country` field — `CountryCode` enum on inputs
324
+
325
+ 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:
326
+
327
+ ```graphql
328
+ # Before (4.x)
329
+ input CheckoutAddressInput {
330
+ country: String! # validated server-side via @Matches(/^[A-Z]{2}$/)
331
+ }
332
+
333
+ # After (5.x)
334
+ input CheckoutAddressInput {
335
+ country: CountryCode! # GraphQL enum — invalid values rejected at validation
336
+ }
337
+ ```
338
+
339
+ Output types keep two distinct fields where applicable: `country` (display name, e.g. "Polska") + `countryCode` (typed enum, e.g. `PL`).
340
+
341
+ ### Address fields — international naming
342
+
343
+ `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:
344
+
345
+ | Before (4.x) | After (5.x) |
346
+ | -------------- | ------------- |
347
+ | `address1` | `streetLine1` |
348
+ | `address2` | `streetLine2` |
349
+ | `province` | `state` |
350
+ | `provinceCode` | `stateCode` |
351
+ | `zip` | `postalCode` |
352
+
353
+ ```graphql
354
+ # Before (4.x)
355
+ fragment MailingAddress on MailingAddress {
356
+ address1
357
+ address2
358
+ province
359
+ zip
360
+ }
361
+
362
+ # After (5.x)
363
+ fragment MailingAddress on MailingAddress {
364
+ streetLine1
365
+ streetLine2
366
+ state
367
+ postalCode
368
+ }
369
+
370
+ # Mutation input
371
+ mutation AddAddress($input: MailingAddressInput!) {
372
+ customerAddAddress(input: $input) { ... }
373
+ }
374
+ # variables (4.x): { address1: "...", province: "...", zip: "..." }
375
+ # variables (5.x): { streetLine1: "...", state: "...", postalCode: "..." }
376
+ ```
377
+
378
+ **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).
379
+
380
+ **`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.
381
+
382
+ ### Costs and totals — nested wrappers everywhere
383
+
384
+ 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:
385
+
386
+ ```graphql
387
+ # CartCost — drop "Amount" suffix from field names (Money type implies amount)
388
+ fragment CartCost on CartCost {
389
+ subtotal {
390
+ ...Money
391
+ } # was: subtotalAmount
392
+ total {
393
+ ...Money
394
+ } # was: totalAmount
395
+ totalTax {
396
+ ...Money
397
+ } # was: totalTaxAmount
398
+ totalDuty {
399
+ ...Money
400
+ } # was: totalDutyAmount
401
+ checkoutCharge {
402
+ ...Money
403
+ } # was: checkoutChargeAmount
404
+ }
405
+
406
+ # CartLineCost — drop "Amount" suffix + "amountPerQuantity" → "pricePerUnit"
407
+ fragment CartLineCost on CartLineCost {
408
+ pricePerUnit {
409
+ ...Money
410
+ } # was: amountPerQuantity
411
+ subtotal {
412
+ ...Money
413
+ } # was: subtotalAmount
414
+ total {
415
+ ...Money
416
+ } # was: totalAmount
417
+ compareAtPricePerUnit {
418
+ ...Money
419
+ } # was: compareAtAmountPerQuantity
420
+ }
421
+
422
+ # CheckoutCost — NEW wrapper, replaces flat checkout fields
423
+ query Checkout {
424
+ checkout {
425
+ cost {
426
+ subtotal {
427
+ ...Money
428
+ } # was: Checkout.subtotalPrice
429
+ total {
430
+ ...Money
431
+ } # was: Checkout.totalPrice
432
+ totalTax {
433
+ ...Money
434
+ } # was: Checkout.totalTax (now nested)
435
+ totalShipping {
436
+ ...Money
437
+ } # was: Checkout.totalShippingPrice
438
+ totalDiscounts {
439
+ ...Money
440
+ } # was: Checkout.totalDiscounts (now nested)
441
+ }
442
+ }
443
+ }
444
+
445
+ # OrderTotals — NEW wrapper, replaces flat order fields
446
+ query CustomerOrder {
447
+ customerOrder {
448
+ totals {
449
+ subtotal {
450
+ ...Money
451
+ } # was: Order.subtotalPrice
452
+ total {
453
+ ...Money
454
+ } # was: Order.totalPrice
455
+ totalTax {
456
+ ...Money
457
+ } # was: Order.totalTax (now nested)
458
+ totalShipping {
459
+ ...Money
460
+ } # was: Order.totalShipping (now nested)
461
+ }
462
+ }
463
+ }
464
+
465
+ # CheckoutLineItem — line price field renames
466
+ fragment CheckoutLineItem on CheckoutLineItem {
467
+ pricePerUnit {
468
+ ...Money
469
+ } # was: unitPrice
470
+ total {
471
+ ...Money
472
+ } # was: totalPrice
473
+ }
474
+ ```
475
+
476
+ Conversion-transparency opt-in fields (`*WithConversion`) are renamed in lockstep:
477
+ - `subtotalAmountWithConversion` → `subtotalWithConversion`
478
+ - `totalAmountWithConversion` → `totalWithConversion`
479
+ - `amountPerQuantityWithConversion` → `pricePerUnitWithConversion`
480
+ - ...and so on.
481
+
482
+ ### Cart line: simplified shape — `variant` replaces `merchandise` union
483
+
484
+ `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:
485
+
486
+ ```graphql
487
+ # Before (4.x)
488
+ fragment CartLine on CartLine {
489
+ id
490
+ quantity
491
+ merchandise {
492
+ ... on ProductVariant {
493
+ id
494
+ title
495
+ price { amount currencyCode }
496
+ }
497
+ }
498
+ cost { ... }
499
+ }
500
+
501
+ # After (5.x) — direct field, no union/inline fragment
502
+ fragment CartLine on CartLine {
503
+ id
504
+ quantity
505
+ variant {
506
+ id
507
+ title
508
+ price { amount currencyCode }
509
+ }
510
+ cost { ... }
511
+ }
512
+ ```
513
+
514
+ Cart connection types renamed for the same reason: `BaseCartLineConnection` / `BaseCartLineEdge` → `CartLineConnection` / `CartLineEdge`. Update fragment selections accordingly:
515
+
516
+ ```graphql
517
+ # Before (4.x)
518
+ query Cart($id: ID!) {
519
+ cart(id: $id) {
520
+ lines(first: 10) {
521
+ edges {
522
+ node {
523
+ ... on CartLine {
524
+ id
525
+ }
526
+ }
527
+ }
528
+ nodes {
529
+ ... on CartLine {
530
+ id
531
+ }
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ # After (5.x)
538
+ query Cart($id: ID!) {
539
+ cart(id: $id) {
540
+ lines(first: 10) {
541
+ edges {
542
+ node {
543
+ id
544
+ }
545
+ }
546
+ nodes {
547
+ id
548
+ }
549
+ }
550
+ }
551
+ }
552
+ ```
553
+
554
+ Cart line input renamed `merchandiseId` → `variantId` (consistent with the output field):
555
+
556
+ ```graphql
557
+ # Before (4.x)
558
+ mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
559
+ cartAddLines(id: $id, lines: $lines) { ... }
560
+ }
561
+ # variables: { lines: [{ merchandiseId: "uuid", quantity: 1 }] }
562
+
563
+ # After (5.x)
564
+ mutation CartAddLines($id: ID!, $lines: [CartLineInput!]!) {
565
+ cartAddLines(id: $id, lines: $lines) { ... }
566
+ }
567
+ # variables: { lines: [{ variantId: "uuid", quantity: 1 }] }
568
+ ```
569
+
570
+ The `variantId` name also matches the `userErrors[].field` path returned for input-related errors (e.g. `field: ["lines", "variantId"]`).
571
+
572
+ ### Recommendations: top-level queries replaced by field resolvers
573
+
574
+ 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:
575
+
576
+ ```graphql
577
+ # Before (4.x)
578
+ query SimilarProducts($productId: String!, $first: Int) {
579
+ similarProducts(productId: $productId, first: $first) {
580
+ items {
581
+ product {
582
+ id
583
+ title
584
+ }
585
+ type
586
+ score
587
+ }
588
+ totalCount
589
+ }
590
+ }
591
+ query CartRecommendations($cartId: String!, $first: Int) {
592
+ cartRecommendations(cartId: $cartId, first: $first) {
593
+ frequentlyBoughtTogether {
594
+ product {
595
+ id
596
+ }
597
+ }
598
+ youMayAlsoLike {
599
+ product {
600
+ id
601
+ }
602
+ }
603
+ }
604
+ }
605
+
606
+ # After (5.x) — same payloads, parent-scoped
607
+ query Product($id: ID!, $first: Int) {
608
+ product(id: $id) {
609
+ recommendations(first: $first) {
610
+ items {
611
+ product {
612
+ id
613
+ title
614
+ }
615
+ type
616
+ score
617
+ }
618
+ totalCount
619
+ }
620
+ }
621
+ }
622
+ query Cart($id: ID!, $first: Int) {
623
+ cart(id: $id) {
624
+ recommendations(first: $first) {
625
+ frequentlyBoughtTogether {
626
+ product {
627
+ id
628
+ }
629
+ }
630
+ youMayAlsoLike {
631
+ product {
632
+ id
633
+ }
634
+ }
635
+ }
636
+ }
637
+ }
638
+ ```
639
+
640
+ 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).
641
+
642
+ ## 4.7.2
643
+
644
+ ### Patch Changes
645
+
646
+ - 7846bdb: Fix `doswiftly dev` port pre-flight on Windows (and any host where another
647
+ process binds the port on IPv6 `::` only): the probe now checks IPv4 and
648
+ IPv6 in parallel and requires BOTH free before marking a port as available.
649
+ Previously an IPv6-only conflict (common on Windows, where Next.js binds
650
+ `::` by default) slipped past the IPv4-only probe — the banner advertised
651
+ `http://localhost:3000` and the framework immediately crashed with
652
+ `EADDRINUSE :::3000`. With this fix `doswiftly dev` falls back to the next
653
+ free port (3001, 3002, …) as documented.
654
+
655
+ Also ship `CHANGELOG.md` inside the npm tarball. Previous releases packed
656
+ only `dist`/`bin`/`templates` (and `schema.graphql`/operations for
657
+ `storefront-operations`, `dist` for `storefront-sdk`), so consumers who
658
+ `npm install @doswiftly/cli` got a version number with no user-visible
659
+ release notes.
660
+
661
+ Wszystkie istotne zmiany w `@doswiftly/storefront-sdk` sa dokumentowane w tym pliku.
662
+ Format oparty na [Keep a Changelog](https://keepachangelog.com/pl/1.1.0/).
663
+ Wersjonowanie zgodne z [Semantic Versioning](https://semver.org/).
664
+
665
+ ## [4.7.1] - 2026-04-17
666
+
667
+ ### Security
668
+
669
+ - Bump `next` devDependency range to `^16.2.3` (GHSA-q4gf-8mx6-v5v3 — Next.js DoS via Server Components). Brak zmian API SDK.
670
+
671
+ ## [4.7.0] - 2026-04-14
672
+
673
+ ### Added
674
+
675
+ - **`CartAttributeSelectionInput`** — nowy typ mirrorujący GraphQL `AttributeSelectionInput` dla konfiguratorów produktu (Faza 1 Unified Product Configurator).
676
+ - **`CartLineInput.attributeSelections`** — opcjonalne customer-filled atrybuty (Finiszer, nr telefonu serwisu itp.). Backend waliduje i snapshotuje pole `surcharge_amount` / `tax_rate`.
677
+ - **`CartLineUpdateInput.attributeSelections`** — update selekcji po stronie koszyka (`null` = preserve, `[]` = clear, tablica = replace).
678
+
679
+ ### Changed
680
+
681
+ - Mutacje `cartLinesAdd` / `cartLinesUpdate` przekazują teraz `attributeSelections` przez GraphQL variables (bez zmian w treści mutacji — typ po stronie schematu backendu).
682
+
683
+ ## [4.6.0] - 2026-04-11
684
+
685
+ ### Breaking Changes
686
+
687
+ - **Image fragment**: `url` field now includes `transform: { maxWidth: 300 }` argument — cart images return CDN-resized thumbnails instead of full-size originals
688
+ - **ImageData interface**: added `thumbhash?: string | null` field
689
+ - **CartLineMerchandise.image**: added `thumbhash` field to image shape
690
+
691
+ ### Added
692
+
693
+ - **Image transforms in cart operations**: CDN serves correctly-sized thumbnails (300px) instead of full originals. Reduces bandwidth ~10x for cart page.
694
+ - **ThumbHash support**: `Image` fragment now requests `thumbhash` field — base64-encoded perceptual placeholder (~40 chars). Decode with `thumbHashToDataURL()` for instant blur previews.
695
+ - **`thumbHashToDataURL(base64Hash)`**: New export from core — decodes ThumbHash to `data:image/bmp;base64,...` URL for Next.js `blurDataURL` prop. Pure math, zero deps, framework-agnostic.
696
+ - **AVIF auto-negotiation**: No `preferredContentType` hardcoded — imgproxy auto-serves AVIF/WEBP based on browser `Accept` header.
697
+
698
+ ## [4.5.0] - 2026-04-11
699
+
700
+ ### Breaking Changes
701
+
702
+ - **CartClient types**: `CartCost` and `CartLineCost` price fields changed from `Money` to `PriceMoney` (adds `baseAmount`, `baseCurrencyCode`, `exchangeRate`, `marginApplied`, `rateTimestamp`, `isConverted`)
703
+ - **Cart.lines**: changed from `{ edges: Array<{ node: CartLine }> }` to `CartLine[]` (flat array — matches backend schema)
704
+ - **CartDiscountAllocation**: field `discountedAmount` renamed to `amount`, added `discountCode` field
705
+
706
+ ### Fixed
707
+
708
+ - CartClient GraphQL operations now match SSOT (backend `storefront-graphql/operations/`)
709
+ - Cart fragment no longer uses `edges`/`nodes` connection pattern for `lines` — backend returns flat array
710
+ - `CartDiscountAllocation` uses correct field name `amount` (was `discountedAmount`) and includes `discountCode`
711
+ - `CartCost` and `CartLineCost` use `PriceMoney` fragment (was `Money`) — enables currency conversion transparency
712
+ - `CartLineCost` includes `subtotalAmount` field (was missing)
713
+ - `CartLine` includes `productType` field (was missing)
714
+ - `CartLineMerchandise` now uses full `ProductVariant` fragment (was inline subset) — includes `originalPrice`, `originalCompareAtPrice`, `available`, `quantityAvailable`, `selectedOptions`, `barcode`, `weight`, `position`
715
+
716
+ ### Added
717
+
718
+ - `PriceMoney` interface in cart types — full currency conversion metadata
719
+ - `SelectedOption` interface in cart types
720
+ - `scripts/validate-cart-operations.cjs` — validates SDK cart fragments match SSOT
721
+ - `pnpm validate:cart` script — runs validation in strict mode
722
+ - Contract test `cart-operations-drift.test.ts` — 40 tests verifying fragment fields, spreads, and structural invariants
723
+
724
+ ## [4.4.0] - 2026-03-29
725
+
726
+ ### Removed
727
+
728
+ - **Image loader usuniety**: `storefrontImageLoader`, `createImageLoader`, `ImageLoaderParams`, `ImageFormat`, `PRESET_WIDTHS` — GraphQL API zwraca gotowe CDN URL-e z `url(transform: { maxWidth: 800 })`. Client-side loader zbedny.
729
+ - Zachowany: `ImageData` type (uzywany przez template do typowania danych z GraphQL)
730
+
731
+ ## [4.3.0] - 2026-03-29
732
+
733
+ ### Changed
734
+
735
+ - **Image loader**: imgproxy path-based URLs zamiast query params
736
+ - `createImageLoader()` przyjmuje konfigurowalny `format` (webp/avif/jpeg/auto)
737
+ - `storefrontImageLoader` uzywa nowego path-based schematu
738
+
739
+ ## [4.2.0] - 2026-03-28
740
+
741
+ ### Added
742
+
743
+ - `storefrontImageLoader` jako globalny image loader dla Next.js (`next.config.ts loaderFile`)
744
+ - Kwantyzacja szerokosc do presetow [150, 320, 640, 750, 828, 1080, 1200, 1600, 1920, 2048]
745
+
746
+ ## [4.1.0] - 2026-03-27
747
+
748
+ ### Added
749
+
750
+ - `createImageLoader()` — factory do custom image loaderow z konfiguracja baseUrl/format
751
+ - Usunieto HMAC signing (Shopify pattern — imgproxy nie wymaga podpisu w path-based mode)
752
+
753
+ ## [4.0.0] - 2026-03-20
754
+
755
+ ### Breaking Changes
756
+
757
+ - **Layered architecture**: split na `core/` (framework-agnostic) + `react/` (adapter)
758
+ - Nowe export paths: `.`, `./react`, `./react/server`, `./cache`
759
+ - ESM-only (usuniete CJS)
760
+ - Usuniety re-export `@tanstack/react-query` — hooki React Query generowane lokalnie w template
761
+ - Store pattern zmieniony na Context-based (zustand/vanilla + React Context)
762
+ - Usuniety module-level singleton pattern
763
+
764
+ ### Added
765
+
766
+ - `createStorefrontClient()` — transport factory z composable middleware pipeline
767
+ - Middleware: `authMiddleware`, `currencyMiddleware`, `languageMiddleware`, `botProtectionMiddleware`, `retryMiddleware`, `timeoutMiddleware`, `errorMiddleware`
768
+ - `CartClient`, `AuthClient` — plain async clients (0 deps, framework-agnostic)
769
+ - `StorefrontError` — zunifikowana klasa bledow z ErrorCodes
770
+ - `StorefrontProvider` — root kompozycji (tworzy store instances via useRef)
771
+ - `createStoreContext()` — generyczny helper Context+Zustand
772
+ - `createCartStore()` z DI pattern (`getActions` getter)
773
+ - `useAuth()`, `useCartManager()`, `useCurrency()`, `useStorefrontClient()`
774
+ - `useHydrated()`, `useAuthHydrated()`, `useDebouncedValue()`
775
+ - `createSetTokenHandler()`, `createClearTokenHandler()` — Web API fabryki
776
+ - `createAuthTokenClient()` — client-side fetch helpers
777
+ - `matchesRoute()` — route protection utility
778
+ - `formatPrice`, `formatPriceRange`, `formatAmount`, `formatDate`, `formatDateTime`, `formatNumber`, `formatPercentage`, `getCurrencySymbol`
779
+ - `sanitizeHtml`, `normalizeConnection`, `assertNoUserErrors`
780
+ - Cache strategies: `cacheNone`, `cacheShort`, `cacheLong`, `cachePrivate`, `cacheCustom`
781
+ - Bot protection: `createBotProtectionManager`, `FallbackBotProtectionManager`, `TurnstileManager`, `EuCaptchaManager`
782
+ - Cookie configs: `AUTH_COOKIE_NAME`, `CURRENCY_COOKIE_NAME`, `LANGUAGE_COOKIE_NAME`, `CART_COOKIE_NAME`
783
+ - `getStorefrontClient()` — server-side factory (react/server)
784
+
785
+ ### Removed
786
+
787
+ - Runtime deps: `graphql-request`, `graphql`, `graphql-tag`, `@graphql-typed-document-node/core`, `@tanstack/react-query`
788
+ - Module-level store singletons (zastapione Context pattern)