@doswiftly/storefront-operations 17.0.0 → 18.1.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.
package/AGENTS.md CHANGED
@@ -27,7 +27,7 @@ consumer's `codegen.ts` references this package's `.graphql` files as
27
27
  live in the consumer's repo.
28
28
 
29
29
  <!-- AUTOGEN:STATS:BEGIN — auto-regenerated, do not edit by hand -->
30
- - **Schema version**: 17.0.0
30
+ - **Schema version**: 18.1.0
31
31
  - **Queries**: 52
32
32
  - **Mutations**: 41
33
33
  - **Fragments**: 104
package/CHANGELOG.md CHANGED
@@ -1,5 +1,555 @@
1
1
  # Changelog
2
2
 
3
+ ## 18.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ff03fd3: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package.
8
+
9
+ ## 18.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - 1d51cc7: Schema MAJOR: nine String/`[String]` image URL fields replaced with `Image`/`[Image!]!`. Every storefront-facing image field now supports `Image.url(transform: ...)` and multi-tenant CDN signing on shop-owned media. Migration touches return, brand, shipping carrier, loyalty benefit, product review, attribute swatch, and payment surfaces.
14
+
15
+ **Why**: This is the final batch of the image-transform pipeline sweep. Previously these nine fields returned a raw string (or list of strings), so storefronts could not request specific sizes, formats, or crops. After this change, every image-bearing field on the storefront API exposes the same `Image` shape as `Product` and `ProductVariant` — pass an `ImageTransformInput` to the `url` argument to get a responsive variant, and rely on imgproxy signing where the merchant uploaded media to our CDN.
16
+
17
+ **Schema diff (9 fields)**:
18
+
19
+ | Type | Before | After |
20
+ | ------------------------- | ----------------------- | ------------------- |
21
+ | `ReturnLineItem` | `imageUrl: String` | `image: Image` |
22
+ | `Brand` | `logo: String` | `logo: Image` |
23
+ | `BrandFilterValue` | `logo: String` | `logo: Image` |
24
+ | `ShippingCarrier` | `logoUrl: String` | `logo: Image` |
25
+ | `TierBenefit` | `icon: String` | `icon: Image` |
26
+ | `ProductReview` | `images: [String]` | `images: [Image!]!` |
27
+ | `AttributeSwatch` | `imageUrl: String` | `image: Image` |
28
+ | `PaymentMethod` | `icon: String` | `icon: Image` |
29
+ | `PaymentMethodInstrument` | `brandImageUrl: String` | `brandImage: Image` |
30
+
31
+ **Migration — pick a thumbnail fragment and replace selections**:
32
+
33
+ The storefront-operations package ships an `ImageThumbnail` fragment (`{ id, url(transform: { maxWidth: 300 }), altText, width, height, thumbhash }`) plus `ImageCard` (maxWidth 800) and `ImageFull` (maxWidth 1600). Pick the size that matches where you render the image.
34
+
35
+ ```diff
36
+ fragment ReturnItem on ReturnItem {
37
+ id
38
+ variantTitle
39
+ - imageUrl
40
+ + image {
41
+ + ...ImageThumbnail
42
+ + }
43
+ }
44
+
45
+ fragment ShippingCarrier on ShippingCarrier {
46
+ id
47
+ name
48
+ - logoUrl
49
+ + logo {
50
+ + ...ImageThumbnail
51
+ + }
52
+ serviceCode
53
+ }
54
+
55
+ fragment AttributeSwatch on AttributeSwatch {
56
+ colorHex
57
+ - imageUrl
58
+ + image {
59
+ + ...ImageThumbnail
60
+ + }
61
+ }
62
+
63
+ fragment ProductReview on ProductReview {
64
+ id
65
+ rating
66
+ - images
67
+ + images {
68
+ + ...ImageThumbnail
69
+ + }
70
+ }
71
+
72
+ fragment LoyaltyTier on LoyaltyTier {
73
+ id
74
+ name
75
+ customBenefits {
76
+ name
77
+ description
78
+ - icon
79
+ + icon {
80
+ + ...ImageThumbnail
81
+ + }
82
+ }
83
+ }
84
+
85
+ fragment PaymentMethod on PaymentMethod {
86
+ id
87
+ name
88
+ type
89
+ - icon
90
+ + icon {
91
+ + ...ImageThumbnail
92
+ + }
93
+ }
94
+
95
+ fragment PaymentMethodInstrument on PaymentMethodInstrument {
96
+ providerCode
97
+ instrumentCode
98
+ displayName
99
+ - brandImageUrl
100
+ + brandImage {
101
+ + ...ImageThumbnail
102
+ + }
103
+ enabled
104
+ }
105
+
106
+ # Brand (used inside MenuItem.resource union and as Product.brand)
107
+ {
108
+ brand {
109
+ id
110
+ handle
111
+ name
112
+ - logo
113
+ + logo {
114
+ + ...ImageThumbnail
115
+ + }
116
+ }
117
+ }
118
+
119
+ # BrandFilterValue (used inside AvailableFilters.brands)
120
+ {
121
+ brands {
122
+ id
123
+ handle
124
+ name
125
+ - logo
126
+ + logo {
127
+ + ...ImageThumbnail
128
+ + }
129
+ productCount
130
+ }
131
+ }
132
+ ```
133
+
134
+ **Field renames** (additional to type changes):
135
+ - `ShippingCarrier.logoUrl` → `ShippingCarrier.logo`
136
+ - `AttributeSwatch.imageUrl` → `AttributeSwatch.image`
137
+ - `ReturnLineItem.imageUrl` → `ReturnLineItem.image`
138
+ - `PaymentMethodInstrument.brandImageUrl` → `PaymentMethodInstrument.brandImage`
139
+
140
+ Rename + type change happen in the same release; you have to update both the field name and the selection set.
141
+
142
+ **Inline transform alternative**: if you do not want to use `ImageThumbnail`, you can request the URL with an explicit transform — the result is identical:
143
+
144
+ ```graphql
145
+ {
146
+ carrier {
147
+ logo {
148
+ url(transform: { maxWidth: 64, preferredContentType: WEBP })
149
+ altText
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ **Migration checklist**:
156
+ - [ ] Update every fragment listed above so the selection set requests `url`/`altText`/etc. instead of treating the field as a scalar.
157
+ - [ ] Rename selections for the four fields that changed name (`logoUrl`/`imageUrl`/`brandImageUrl`).
158
+ - [ ] Re-run codegen so your generated TypedDocumentNode bindings reflect the new shape.
159
+ - [ ] If your UI rendered the old string URL directly, switch to consuming `field.url` (and optionally `field.altText`); pick the transform size that matches your component.
160
+ - [ ] `ProductReview.images` is now a non-null list of non-null `Image` values — drop any defensive null handling around individual elements but keep the empty-array case.
161
+
162
+ **No backend-input breakage**: `ReviewCreateInput.images` still takes `[String]` URLs from the customer at submission time. Only the response field changed shape.
163
+
164
+ - e28e902: Payment schema naming overhaul — one coherent provider / method / instrument model.
165
+
166
+ **Why**: payment fields mixed three concepts under inconsistent names — a `ProviderCode` enum, a `PaymentMethodInstrument` object whose fields were `instrumentCode` / `providerCode`, and `*Code`-suffixed mutation inputs. Every payment field now follows a single convention: **provider** (who processes the payment — `PAYU`, `PRZELEWY24`), **method** (the category the buyer picks — `BLIK`, `CARD`), **instrument** (the concrete option within a method — a specific BLIK code or bank deep-link).
167
+
168
+ **Breaking**:
169
+ 1. Enum `ProviderCode` → `PaymentProvider` (values unchanged: `PAYU`, `PRZELEWY24`, `STRIPE`, ...).
170
+ 2. Object `PaymentMethodInstrument` → `PaymentInstrument`; field `instrumentCode` → `code`, field `providerCode` → `provider`.
171
+ 3. Enums `PaymentMethodInstrumentType` → `PaymentInstrumentType`, `PaymentMethodInstrumentDisplayHint` → `PaymentInstrumentDisplayHint`.
172
+ 4. `Cart.selectedPaymentInstrumentCode` → `Cart.selectedPaymentInstrument`.
173
+ 5. `CartSelectPaymentMethodInput`: `preferredProviderCode` → `preferredProvider`, `preferredInstrumentCode` → `preferredInstrument`.
174
+ 6. `CartSelectPaymentMethodInput.preferredProvider` is now the `PaymentProvider` enum (UPPERCASE: `PAYU`, `PRZELEWY24`, ...) instead of a lowercase `String`. It matches the output — `PaymentInstrument.provider` and `PaymentMethod.preferredProvider` are already `PaymentProvider` — so you read the value off the API and pass it straight back, no case conversion. The schema now self-documents the selectable gateways. (`preferredInstrument` stays `String` — instrument codes are gateway-specific.)
175
+
176
+ **Usage example**:
177
+
178
+ ```graphql
179
+ # before
180
+ fragment Instrument on PaymentMethodInstrument {
181
+ providerCode
182
+ instrumentCode
183
+ displayName
184
+ }
185
+ mutation Select($input: CartSelectPaymentMethodInput!) {
186
+ cartSelectPaymentMethod(input: $input) {
187
+ cart {
188
+ selectedPaymentInstrumentCode
189
+ }
190
+ }
191
+ }
192
+ # $input: { cartId, methodType: BLIK, preferredProviderCode: "payu", preferredInstrumentCode: "blik" }
193
+
194
+ # after
195
+ fragment Instrument on PaymentInstrument {
196
+ provider
197
+ code
198
+ displayName
199
+ }
200
+ mutation Select($input: CartSelectPaymentMethodInput!) {
201
+ cartSelectPaymentMethod(input: $input) {
202
+ cart {
203
+ selectedPaymentInstrument
204
+ }
205
+ }
206
+ }
207
+ # $input: { cartId, methodType: BLIK, preferredProvider: PAYU, preferredInstrument: "blik" }
208
+ # ^ preferredProvider is the PaymentProvider enum (UPPERCASE) — read it from
209
+ # `instrument.provider` / `method.preferredProvider` and pass it back verbatim.
210
+ ```
211
+
212
+ ```ts
213
+ // SDK types — before
214
+ import type {
215
+ PaymentMethodInstrument,
216
+ ProviderCode,
217
+ } from "@doswiftly/storefront-sdk";
218
+ const code = instrument.instrumentCode;
219
+ const who = instrument.providerCode;
220
+
221
+ // after
222
+ import type {
223
+ PaymentInstrument,
224
+ PaymentProvider,
225
+ } from "@doswiftly/storefront-sdk";
226
+ const code = instrument.code;
227
+ const who = instrument.provider;
228
+ ```
229
+
230
+ The pre-built `<PaymentInstrumentSection>` / `<PaymentInstrumentTile>` component props (`selectedInstrumentCode`, `onSelectInstrument`) are unchanged — only the instrument data shape (`.code`) changed.
231
+
232
+ **Migration checklist for existing storefronts**:
233
+ - [ ] Rename type references: `PaymentMethodInstrument` → `PaymentInstrument`, `ProviderCode` → `PaymentProvider`, `PaymentMethodInstrumentType`/`PaymentMethodInstrumentDisplayHint` → `PaymentInstrumentType`/`PaymentInstrumentDisplayHint`
234
+ - [ ] Field access: `instrument.instrumentCode` → `instrument.code`, `instrument.providerCode` → `instrument.provider`
235
+ - [ ] Cart selection: `cart.selectedPaymentInstrumentCode` → `cart.selectedPaymentInstrument`
236
+ - [ ] Mutation input: `preferredProviderCode` → `preferredProvider`, `preferredInstrumentCode` → `preferredInstrument`
237
+ - [ ] Mutation input value: pass the `PaymentProvider` enum (UPPERCASE) to `preferredProvider`, not a lowercase string — `preferredProvider: "payu"` → `preferredProvider: PAYU`. Drop any `.toLowerCase()` you applied to `instrument.provider` before sending it back.
238
+ - [ ] If you hand-write GraphQL operations: update fragments + re-run codegen against the published `schema.graphql`
239
+
240
+ - 159184c: `ShipmentItem.imageUrl: String` replaced with `ShipmentItem.image: Image`.
241
+
242
+ **Why**: The previous `imageUrl` field returned a raw URL without transform support and was unreachable due to a resolver mapping bug (snapshot field names did not match what the storefront-facing mapper expected). The new `image: Image` field aligns shipment item images with `Product` and `ProductVariant` patterns — storefronts can request specific sizes, formats, and crops via `image { url(transform: { maxWidth: 200, preferredContentType: WEBP }) }`.
243
+
244
+ **Image source**: live variant image with snapshot fallback. The variant is loaded per-request via DataLoader (no N+1 cost when rendering many shipment items at once), and the resolved URL passes through the storefront's image transform pipeline so the requested dimensions/format are honored. When the variant has been deleted since the shipment was created, `image` returns `null` — render a placeholder client-side. The `title`, `variantTitle`, and `sku` fields stay as immutable snapshots from the time of fulfillment, so the tracking page still shows what was shipped even after a product is removed from the catalog.
245
+
246
+ **Migration**:
247
+
248
+ ```diff
249
+ fragment ShipmentItem on ShipmentItem {
250
+ id
251
+ title
252
+ variantTitle
253
+ sku
254
+ quantity
255
+ - imageUrl
256
+ + image {
257
+ + ...ImageThumbnail
258
+ + }
259
+ }
260
+ ```
261
+
262
+ Or, if you prefer to inline the transform without using the `ImageThumbnail` fragment:
263
+
264
+ ```diff
265
+ fragment ShipmentItem on ShipmentItem {
266
+ id
267
+ title
268
+ variantTitle
269
+ sku
270
+ quantity
271
+ - imageUrl
272
+ + image {
273
+ + url(transform: { maxWidth: 200, preferredContentType: WEBP })
274
+ + altText
275
+ + }
276
+ }
277
+ ```
278
+
279
+ **Migration checklist**:
280
+ - [ ] Replace `imageUrl` selections in your `ShipmentItem` fragments / inline queries with `image { ...ImageThumbnail }` (or inline transform).
281
+ - [ ] Handle the `image: null` case in your ShipmentItem rendering (variant deleted after fulfillment — render a placeholder).
282
+ - [ ] Re-run codegen so your TypedDocumentNode bindings reflect the new shape.
283
+ - [ ] No SDK code changes are required beyond the regenerated operation types — the linked SDK bump exists to keep `@doswiftly/storefront-sdk` in version parity with `@doswiftly/storefront-operations`.
284
+
285
+ - cc7edbe: SDK-BFF customer authentication — same-origin route handlers + automatic session refresh.
286
+
287
+ **Why**: Customer auth now runs entirely same-origin on your storefront domain through a backend-for-frontend layer. The browser never calls the commerce API for auth, and the refresh token never reaches JavaScript — it lives in a first-party httpOnly cookie read only server-side and exchanged server-to-server. This behaves identically on a platform subdomain, a custom domain, and off-platform hosting (e.g. Vercel), because the route handlers live on your own domain rather than depending on any edge layer.
288
+
289
+ **New**
290
+ - `createStorefrontAuthRoute({ apiUrl, shopSlug, isTrustedOrigin? })` (from `@doswiftly/storefront-sdk/react/server`) generates the `login` / `refresh` / `logout` / `whoami` route handlers. Mount once:
291
+
292
+ ```ts
293
+ // app/api/auth/[action]/route.ts
294
+ import {
295
+ createStorefrontAuthRoute,
296
+ trustedForwardedHostValidator,
297
+ } from "@doswiftly/storefront-sdk/react/server";
298
+
299
+ export const { GET, POST } = createStorefrontAuthRoute({
300
+ apiUrl: process.env.NEXT_PUBLIC_API_URL!,
301
+ shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
302
+ isTrustedOrigin: trustedForwardedHostValidator, // when running behind a reverse proxy
303
+ });
304
+ ```
305
+
306
+ - `getInitialAuth()` (server-only) reads the first-party cookies and returns `{ isAuthenticated, accessToken, expiresAt }` to seed the provider on the first render — no flash of signed-out UI, no extra round-trip:
307
+
308
+ ```tsx
309
+ // app/layout.tsx (Server Component)
310
+ const { isAuthenticated, accessToken, expiresAt } = await getInitialAuth();
311
+ return (
312
+ <StorefrontProvider
313
+ initialIsAuthenticated={isAuthenticated}
314
+ initialAccessToken={accessToken}
315
+ initialExpiresAt={expiresAt}
316
+ /* ...config, shopData */
317
+ >
318
+ {children}
319
+ </StorefrontProvider>
320
+ );
321
+ ```
322
+
323
+ - `AuthClient.refreshSession()` refreshes the session via the same-origin `POST /api/auth/refresh`, returning `{ accessToken, expiresAt }`. The proactive scheduler (`autoRefresh`, on by default in the browser) and the reactive 401 retry now use it — so an already-expired access token still refreshes silently.
324
+
325
+ **Breaking**
326
+ 1. Automatic refresh (the proactive scheduler and the 401 retry) now goes through `POST /api/auth/refresh` instead of a GraphQL refresh. You **must** mount `createStorefrontAuthRoute` for refresh to work.
327
+ 2. `useSessionRefresh` no longer accepts an `authBasePath` option — the base path comes from the provider-configured auth client.
328
+ 3. The refresh cookie is scoped to the auth base path (so logout can revoke the session), and the readable session-expiry hint cookie is long-lived (so a returning visitor is not shown as signed-out on a cold start).
329
+
330
+ **Migration checklist**
331
+ - [ ] Add `app/api/auth/[action]/route.ts` exporting `createStorefrontAuthRoute(...)`.
332
+ - [ ] Seed the provider from `getInitialAuth()` in your root layout (Server Component).
333
+ - [ ] Remove any `authBasePath` argument passed to `useSessionRefresh`.
334
+ - [ ] Subscribe with `useSessionExpired(cb)` to react globally (notice + redirect to sign-in).
335
+ - [ ] Behind a reverse proxy that rewrites `Host`, pass `isTrustedOrigin: trustedForwardedHostValidator`.
336
+
337
+ **Standards reference**: RFC 6265 (cookie path-matching), RFC 9700 (OAuth 2.0 security best current practice — token endpoints are public; security is protocol-level, not network allowlists).
338
+
339
+ `@doswiftly/storefront-operations` is version-synced with this release (linked pair); it has no operation changes in this entry.
340
+
341
+ ### Minor Changes
342
+
343
+ - 9969bdd: Keep customer sessions alive automatically — proactive refresh + session-expiry awareness.
344
+
345
+ **Why**: an active buyer should never be logged out mid-session. The SDK now renews the access token for you, ahead of expiry, and exposes a single global signal for when a session truly cannot be saved.
346
+
347
+ **Additive (backward-compatible)**:
348
+ 1. **Automatic proactive refresh** — on by default in the browser (off on the server): `<StorefrontProvider>` renews the access token shortly before it expires, reschedules from each new expiry, and recovers on tab wake. Nothing to pass and nothing to copy into your app; opt out with `autoRefresh={false}`. This is the primary protection — in the happy path the token is renewed before it ever expires.
349
+ 2. **Global session-expired signal** — `useSessionExpired((event) => { ... })` fires when the session cannot be renewed (a refresh failed on tab wake, or a reactive refresh after a 401 also failed). Use it to show a notice and redirect to sign-in. The new `ErrorCodes.SESSION_EXPIRED` names the condition.
350
+ 3. **Reactive recovery on a 401** — if a read request comes back with HTTP 401, the SDK runs a single, deduped refresh and replays the request automatically; a state-changing request never auto-retries — it bails and emits the session-expired signal instead.
351
+ 4. **Session expiry in state** — the auth store now tracks `expiresAt` (ISO 8601), populated from login, refresh and the current-customer query; the access token stays in memory only and `expiresAt` is never persisted. A new `sessionExpiresAt` field is available on the `Customer` type, and `<StorefrontProvider initialExpiresAt>` seeds it during server rendering for an instant cold-start schedule.
352
+
353
+ **Usage**:
354
+
355
+ ```tsx
356
+ // works out of the box — proactive refresh is on by default in the browser
357
+ // (nothing to pass; opt out with autoRefresh={false})
358
+ <StorefrontProvider config={config} shopData={shopData}>
359
+ {children}
360
+ </StorefrontProvider>;
361
+
362
+ // anywhere near the root — react globally when a session could not be saved
363
+ useSessionExpired(() => {
364
+ toast("Your session expired — please sign in again.");
365
+ router.push("/login");
366
+ });
367
+ ```
368
+
369
+ **Migration**: none — everything is additive. `autoRefresh` is opt-out (default-on in the browser) and existing manual refresh flows keep working. The proactive refresh syncs the renewed token through the same `/api/auth/set-token` route you already use for login — no extra wiring. Point it elsewhere with `authBasePath` if your auth routes are mounted on a different base path.
370
+
371
+ ```
372
+
373
+ ```
374
+
375
+ - 5525fd4: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package — see the `@doswiftly/storefront-sdk` changelog for the additive `debug: 'verbose'` mode and `DebugOptions` granular controls.
376
+ - 5525fd4: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package — see the `@doswiftly/storefront-sdk` changelog for the additive runtime const exports of schema enums.
377
+ - 9b62091: `PaymentMethodType` exposes two additional categories that the platform already supports server-side: `WALLET` (Apple Pay / Google Pay / Visa Mobile) and `INSTALLMENT` (Klarna, PayPo, Twisto, Alior Raty). Storefront code can now branch on them without falling through to `OTHER`.
378
+
379
+ **Why** — the GraphQL `PaymentMethodType` enum had drifted away from the underlying domain enum: PayU and Przelewy24 adapters were already mapping payByLink / method names to `WALLET` and `INSTALLMENT` (Apple Pay, Google Pay, BNPL providers), but the public GraphQL enum only declared `CARD / BLIK / BANK_TRANSFER / CASH_ON_DELIVERY / OTHER`. A storefront calling `getAvailablePaymentMethods` against a shop with Apple Pay enabled would crash with `Enum "PaymentMethodType" cannot represent value: "WALLET"`. The enum is now sourced from a single domain definition — drift between the platform's internal enum and the public GraphQL enum is structurally impossible going forward.
380
+
381
+ **Additive (backward-compatible)**:
382
+
383
+ Two new runtime values exposed by the const + type alias (per `enumsAsConst` codegen):
384
+
385
+ ```ts
386
+ import { PaymentMethodType } from "@doswiftly/storefront-sdk";
387
+
388
+ PaymentMethodType.Wallet; // 'WALLET'
389
+ PaymentMethodType.Installment; // 'INSTALLMENT'
390
+
391
+ Object.values(PaymentMethodType);
392
+ // ['BANK_TRANSFER', 'BLIK', 'CARD', 'CASH_ON_DELIVERY', 'INSTALLMENT', 'OTHER', 'WALLET']
393
+ ```
394
+
395
+ The TypeScript union type widens correspondingly to include `'WALLET' | 'INSTALLMENT'`. Existing code that handled `'OTHER'` as a catch-all keeps working — methods previously falling through to `OTHER` now report their specific category, and any exhaustive switch on `PaymentMethodType` gets a compile error pointing at the missing case.
396
+
397
+ **Usage example** — render brand-correct iconography for wallet checkouts:
398
+
399
+ ```tsx
400
+ import { PaymentMethodType } from "@doswiftly/storefront-sdk";
401
+
402
+ function PaymentMethodIcon({ type }: { type: PaymentMethodType }) {
403
+ switch (type) {
404
+ case PaymentMethodType.Card:
405
+ return <CardIcon />;
406
+ case PaymentMethodType.Blik:
407
+ return <BlikIcon />;
408
+ case PaymentMethodType.BankTransfer:
409
+ return <BankIcon />;
410
+ case PaymentMethodType.Wallet:
411
+ return <WalletIcon />; // NEW — Apple Pay / Google Pay
412
+ case PaymentMethodType.Installment:
413
+ return <InstallmentIcon />; // NEW — Klarna / PayPo / Twisto
414
+ case PaymentMethodType.CashOnDelivery:
415
+ return <CashIcon />;
416
+ case PaymentMethodType.Other:
417
+ return <GenericPaymentIcon />;
418
+ }
419
+ }
420
+ ```
421
+
422
+ **Migration checklist** — none required for storefronts that already routed `OTHER` to a generic icon:
423
+ - [ ] (Optional) Add explicit branches for `Wallet` / `Installment` in payment method UI to render brand-correct iconography (Apple Pay logo, Klarna logo, etc.) instead of the generic fallback.
424
+ - [ ] (Optional) If you ran exhaustive `switch (method.type)` blocks asserted with `never`-check, add the two new cases — the compiler points at them on the next build.
425
+
426
+ **Standards reference** — Apple Pay / Google Pay are typically classified under "digital wallet" payment methods in industry catalogues (Stripe, Adyen, Braintree all use `wallet` as a top-level category). `INSTALLMENT` covers Buy-Now-Pay-Later providers that present multi-instalment options at checkout.
427
+
428
+ - 78bd561: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package — see the `@doswiftly/storefront-sdk` changelog for the additive `<StorefrontProvider initialAccessToken>` prop and `authStore.setAuth` signature relax.
429
+ - 5525fd4: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package — see the `@doswiftly/storefront-sdk` changelog for the additive `useCartManager` checkout suite and `complete()` auto-cleanup.
430
+ - 468f70b: Version sync with `@doswiftly/storefront-sdk` (linked release group). No code change in this package — see the `@doswiftly/storefront-sdk` changelog for the additive `useCartManager({ initialCartId })` server-known seed.
431
+
432
+ ### Patch Changes
433
+
434
+ - 9bac7e9: Fix stale field names in the `CartSelectPaymentMethodInput` description. The input's description still referenced `preferredProviderCode` / `preferredInstrumentCode` after those fields were renamed to `preferredProvider` / `preferredInstrument`. Description-only correction — field names, types and behavior are unchanged.
435
+ - 9857460: Polished all GraphQL schema descriptions to consistent professional English (documentation only — no contract changes).
436
+ - a5a3f4b: Image transform pipeline extended to shop-owned content surfaces: `BlogCategory.image`, `BlogPost.featuredImage`, `LoyaltyReward.image`, and `MenuItem.image`.
437
+
438
+ These four fields already accepted an `Image.url(transform: ...)` selection, but the URL was not threaded through the storefront CDN context, so any URL stored as a relative path (rather than a full https URL) skipped the imgproxy signing step. Now all four resolvers build the CDN context per query and pass it to `mapImageToGraphQL`, so transform-aware URLs honor multi-tenant signing on shop-owned media.
439
+
440
+ No schema changes. Selections that already used `image { url(transform: ...) }` keep working — the only behavior change is that URLs stored as relative paths are now CDN-signed before transforms apply. URLs stored as absolute https values keep returning unchanged (the helper falls back to the raw value when there is no path component).
441
+
442
+ **What this enables**:
443
+
444
+ ```graphql
445
+ query BlogList {
446
+ blogPosts(first: 10) {
447
+ edges {
448
+ node {
449
+ title
450
+ featuredImage {
451
+ url(transform: { maxWidth: 800, preferredContentType: WEBP })
452
+ altText
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }
458
+ ```
459
+
460
+ The same `image { url(transform: ...) }` pattern is also valid on `BlogCategory`, `LoyaltyReward`, and `MenuItem`.
461
+
462
+ **No migration required.**
463
+
464
+ - 0c0205d: Image transform support extended to `Cart.lines[].variant.image`, `Order.lineItems[].variant.image`, `Collection.image`, and `Category.image`.
465
+
466
+ Previously these four image fields returned a plain object that did not pass through the storefront image pipeline, so calling `image { url(transform: { maxWidth: 200, preferredContentType: WEBP }) }` silently returned a raw URL without applying the requested size/format. Now all four resolvers map images through the same transform pipeline used by `Product` and `ProductVariant`, so `Image.url(transform: ...)` honors the requested dimensions and format.
467
+
468
+ No schema changes — selections that already used `image { url }` keep working and start receiving the transform-aware URL when a transform argument is supplied. Selections that already used `image { url(transform: ...) }` now apply the transform end-to-end.
469
+
470
+ **What you can do now**:
471
+
472
+ ```graphql
473
+ query CartThumbnails {
474
+ cart {
475
+ lines {
476
+ edges {
477
+ node {
478
+ variant {
479
+ image {
480
+ url(transform: { maxWidth: 200, preferredContentType: WEBP })
481
+ altText
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ ```
490
+
491
+ The same `image { url(transform: ...) }` pattern is now valid on `Order.lineItems[].variant`, `Collection.image`, and `Category.image`.
492
+
493
+ **No migration required** — existing storefronts continue to work. To take advantage of transforms on these surfaces, switch hardcoded sizes in your `<img src>` rendering to a parametrized `url(transform: ...)` selection.
494
+
495
+ - 9c55b39: Export the payment enums `PaymentProvider`, `PaymentInstrumentType`, `PaymentInstrumentDisplayHint` and `PaymentMethodUnavailableReason` from the package root.
496
+
497
+ **Why**: these enums were already generated but only reachable via a deep import path, which reads as private API. They are now re-exported from the package entry point alongside the other schema enums (`PaymentMethodType`, `DeliveryType`, ...), so you can type a wrapper around `cartSelectPaymentMethod` — whose `preferredProvider` input is the `PaymentProvider` enum — without reaching into internals.
498
+
499
+ **Additive (backward-compatible)**: each enum is both a runtime value (`Object.values(PaymentProvider)`) and a type (`import type { PaymentProvider }`). No existing import changes.
500
+
501
+ **Usage example**:
502
+
503
+ ```ts
504
+ import { PaymentProvider } from "@doswiftly/storefront-sdk";
505
+
506
+ // Type a wrapper prop:
507
+ type SelectArgs = { preferredProvider?: PaymentProvider };
508
+
509
+ // Or read the value at runtime:
510
+ const all = Object.values(PaymentProvider); // ['PAYU', 'PRZELEWY24', ...]
511
+ ```
512
+
513
+ **Migration checklist**: optional — if you used a deep import to reach these enums, switch to the package root import.
514
+
515
+ `@doswiftly/storefront-operations` carries a version-sync bump only — no operation or schema change.
516
+
517
+ - 8c0d8fa: Expose `operations.json` through the package `exports` map.
518
+
519
+ **Why**: Documentation generators, MDX components, and other build-time tools need structural access to operation metadata (name, kind, section, description, variables, fragmentRefs, body) without parsing raw `.graphql` files. The `operations.json` file was already published in the npm tarball; this change makes it consumable via a typed subpath import.
520
+
521
+ **Additive (backward-compatible)**:
522
+
523
+ ```ts
524
+ // New: structural import of all operations
525
+ import operations from "@doswiftly/storefront-operations/operations.json";
526
+
527
+ console.log(operations.queries.length); // 52
528
+ console.log(operations.mutations.length); // 41
529
+
530
+ // Existing imports unchanged:
531
+ import schema from "@doswiftly/storefront-operations/schema.graphql";
532
+ ```
533
+
534
+ **Usage example** — build-time docs generation:
535
+
536
+ ```ts
537
+ import operations from "@doswiftly/storefront-operations/operations.json";
538
+
539
+ const cartOps = operations.mutations.filter(
540
+ (op) => op.section === "Cart Mutations",
541
+ );
542
+
543
+ for (const op of cartOps) {
544
+ console.log(`${op.name}: ${op.description}`);
545
+ console.log(
546
+ ` Variables: ${op.variables.map((v) => `${v.name}: ${v.type}`).join(", ")}`,
547
+ );
548
+ }
549
+ ```
550
+
551
+ **Migration checklist**: none — additive change, existing consumers unaffected.
552
+
3
553
  ## 17.0.0
4
554
 
5
555
  ### Major Changes
package/README.md CHANGED
@@ -380,7 +380,7 @@ full executable body of each operation.
380
380
  | `CartSetShippingAddress` | Phase 3 unify-cart-graphql-surface: wszystkie fulfillment + payment + completion operations teraz na Cart aggregate (zamiast Checkout dual-aggregate). Klient robi typowy checkout flow: cart create/add items → setShipping/Billing/Method → selectPayment → (optional) applyGiftCard → cartComplete → Order created. Sets the shipping address on the cart (full replace, not patch). Triggers cart re-pricing (tax recalculation per address country/region). Address format validated against `CartAddressInput` constraints (firstName/lastName/streetLine1/city/country/postalCode required). Errors: `INVALID_ADDRESS`, `CART_NOT_FOUND`. |
381
381
  | `CartSetBillingAddress` | Sets the billing address on the cart (full replace). Independent of shipping address — pass it explicitly even when "billing same as shipping". Errors: `INVALID_ADDRESS`, `CART_NOT_FOUND`. |
382
382
  | `CartSelectShippingMethod` | Selects a shipping method by `shippingMethodId` (typed `ID!`, a stable shipping-method UUID — NOT a per-request token). The id comes from a list of methods available for the current address + cart subtotal (queryable separately). Errors: `SHIPPING_METHOD_REQUIRED`, `ZIP_CODE_NOT_SUPPORTED`, `CART_NOT_FOUND`. |
383
- | `CartSelectPaymentMethod` | Selects a payment method on the cart by category (`methodType` — BLIK, CARD, BANK_TRANSFER, ...). Optional `preferredProviderId` overrides the merchant priority when the buyer explicitly picks a gateway from `PaymentMethod.providersAvailable`. The backend resolves the gateway routing from `MerchantPaymentConfig` at `cartComplete`; the selection persisted here is the category. Errors: `PAYMENT_METHOD_REQUIRED`, `CART_NOT_FOUND`, `CART_UNAUTHENTICATED`. |
383
+ | `CartSelectPaymentMethod` | Selects a payment method on the cart by category (`methodType` — BLIK, CARD, BANK_TRANSFER, ...). Optional `preferredProvider` overrides the merchant priority when the buyer explicitly picks a gateway from `PaymentMethod.providersAvailable`. The backend resolves the gateway routing from `MerchantPaymentConfig` at `cartComplete`; the selection persisted here is the category. Errors: `PAYMENT_METHOD_REQUIRED`, `CART_NOT_FOUND`, `CART_UNAUTHENTICATED`. |
384
384
  | `CartApplyGiftCard` | Applies a gift card to the cart, stackable with discount codes. Consumption is FIFO: each card consumes `min(remainingBalance, paymentDue)` against the current cart total in the order they were applied. The gift card balance is NOT debited yet — actual deduction happens atomically at `cartComplete`. Errors: `GIFT_CARD_NOT_FOUND`, `GIFT_CARD_DEPLETED`, `GIFT_CARD_UNUSABLE`. |
385
385
  | `CartRemoveGiftCard` | Removes a gift card from the applied list and recalculates FIFO `appliedAmount` for the remaining cards. Since gift card balances are only debited at `cartComplete`, removing before completion has no effect on the underlying gift card balance. |
386
386
  | `CartUpdateGiftCardRecipient` | Sets per-line-item recipient details (name, email, message) for digital gift card products in the cart (line items where the variant represents a gift-card SKU). Required before `cartComplete` for any line item with a gift-card variant. Recipient details propagated to the resulting order. |
@@ -488,7 +488,7 @@ full executable body of each operation.
488
488
  | `Cart` | `Cart` | Full cart shape — totals, line items (paginated up to 100), buyer identity, applied discount codes + allocations, note, custom attributes, plus fulfillment fields (email/phone/addresses/shipping method/payment method/applied gift cards). Spread on the cart drawer / cart page; refetch after every cart mutation including completion lifecycle. |
489
489
  | `CartShippingMethod` | `CartShippingMethod` | Shipping method selected on a cart (D8 term unification — wcześniej ShippingRate). Returned przez `cart.selectedShippingMethod` po `cartSelectShippingMethod` mutation. |
490
490
  | `CartAppliedGiftCard` | `CartAppliedGiftCard` | Gift card applied to a cart — `id` is the stable handle the storefront passes to `cartRemoveGiftCard` (no need to keep the raw code around). Plus masked code for display, last 4 chars for matching, applied amount + remaining balance. Balance NIE debited yet — actual deduction atomically at `cartComplete`. |
491
- | `CartSelectedPaymentMethod` | `PaymentMethod` | Payment method (integration provider) selected on a cart — id, name, provider code, type category (CARD/BLIK/BANK_TRANSFER/etc.), icon, description, isDefault, supportedCurrencies. Storefront UI używa do iconography + selection display. |
491
+ | `CartSelectedPaymentMethod` | `PaymentMethod` | Payment method (integration provider) selected on a cart — id, name, provider code, type category (CARD/BLIK/BANK_TRANSFER/etc.), icon image (transformable), description, isDefault, supportedCurrencies. Storefront UI używa do iconography + selection display. |
492
492
 
493
493
  #### Shop
494
494
 
@@ -509,8 +509,8 @@ full executable body of each operation.
509
509
 
510
510
  | Fragment | On Type | Description |
511
511
  | --- | --- | --- |
512
- | `PaymentMethodInstrument` | `PaymentMethodInstrument` | A single concrete instrument exposed by a gateway provider (BLIK code, branded bank, wallet, card brand). Pass `instrumentCode` as `preferredInstrumentCode` in `cartSelectPaymentMethod` for direct deep-link to that instrument screen on the gateway. `displayHint` is a semantic UX hint (`PIN_ENTRY`, `WALLET_TAP`, `BANK_LIST`, `CARD_FORM`) — storefront branches rendering accordingly. `enabled: false` means the gateway reported the instrument as temporarily disabled — gray out, don't hide. |
513
- | `PaymentMethod` | `PaymentMethod` | Single payment method enabled for the shop — method-centric. `type` is the category (CARD, BLIK, BANK_TRANSFER, CASH_ON_DELIVERY, OTHER) — drive iconography here. `provider`, `providersAvailable`, `preferredProvider` are `ProviderCode` enum (UPPERCASE: `PAYU`, `PRZELEWY24`, ...). `available` is `false` when the resolving gateway is temporarily unavailable or reported the method as disabled — gray-out the tile, don't hide it; `unavailableReason` carries the diagnostic. `instruments` lists concrete gateway-side instruments (BLIK code, branded banks, wallets) when the gateway exposes granular data — pass `instrumentCode` as `preferredInstrumentCode` in `cartSelectPaymentMethod` for direct deep-link to that instrument screen on the gateway. Spread on the checkout payment step. |
512
+ | `PaymentInstrument` | `PaymentInstrument` | A single concrete instrument exposed by a gateway provider (BLIK code, branded bank, wallet, card brand). Pass `code` as `preferredInstrument` in `cartSelectPaymentMethod` for direct deep-link to that instrument screen on the gateway. `displayHint` is a semantic UX hint (`PROMINENT_BUTTON`, `BRANDED_TILE`, `DROPDOWN_OPTION`, `RADIO_OPTION`) — storefront branches rendering accordingly. `enabled: false` means the gateway reported the instrument as temporarily disabled — gray out, don't hide. |
513
+ | `PaymentMethod` | `PaymentMethod` | Single payment method enabled for the shop — method-centric. `type` is the category (CARD, BLIK, BANK_TRANSFER, INSTALLMENT, WALLET, CASH_ON_DELIVERY, OTHER) — drive iconography here. `provider`, `providersAvailable`, `preferredProvider` are `PaymentProvider` enum (UPPERCASE: `PAYU`, `PRZELEWY24`, ...). `available` is `false` when the resolving gateway is temporarily unavailable or reported the method as disabled — gray-out the tile, don't hide it; `unavailableReason` carries the diagnostic. `instruments` lists concrete gateway-side instruments (BLIK code, branded banks, wallets) when the gateway exposes granular data — pass `code` as `preferredInstrument` in `cartSelectPaymentMethod` for direct deep-link to that instrument screen on the gateway. Spread on the checkout payment step. |
514
514
  | `AvailablePaymentMethods` | `AvailablePaymentMethods` | Active payment methods list with the merchant's `defaultMethod`. Returned by the `availablePaymentMethods` query. |
515
515
  | `PaymentSession` | `PaymentSession` | Initiated payment session for an order — returned by the `paymentCreate` mutation. Branch on `flow`: `ONLINE_REDIRECT` (redirect the browser to `redirectUrl`), `ONLINE_EMBEDDED` (render an in-page widget with `clientSecret`), `INSTANT_DIRECT` (settled with no UI — read `status`). `expiresAt` is ISO 8601, present when the gateway session has a TTL. |
516
516
  | `PaymentWarning` | `PaymentWarning` | Non-blocking signal emitted alongside `userErrors[]` in `paymentCreate` payload — backend state change context (e.g. an auto-clear of preselected instrument after gateway rejection). Pre-translated by backend per shop locale. `code === 'INSTRUMENT_CLEARED_FOR_RETRY'` signals storefront that the next `paymentCreate` will land on the gateway default landing page (server-side cleared `Order.paymentInstrumentCode` after `INSTRUMENT_PRESELECTION_FAILED`). `retryHint` is optional context for UI dispatch — currently unused for `INSTRUMENT_CLEARED_FOR_RETRY`, reserved for future warning codes. |
@@ -521,7 +521,7 @@ full executable body of each operation.
521
521
  | --- | --- | --- |
522
522
  | `ShipmentEvent` | `ShipmentEvent` | One tracking event in the shipment timeline — status, description, location, timestamp. Spread on the tracking page timeline. |
523
523
  | `ShipmentPackage` | `ShipmentPackage` | Physical package metadata (weight + dimensions). Used in the merchant's label generation; storefronts rarely render this directly. |
524
- | `ShipmentItem` | `ShipmentItem` | One item in a shipment — title, variant, sku, quantity, image URL. Spread inside `Shipment.items[]` for the tracking page item list. |
524
+ | `ShipmentItem` | `ShipmentItem` | One item in a shipment — title, variant, sku, quantity, live variant image (transformable). Spread inside `Shipment.items[]` for the tracking page item list. `image` is live from the current variant (snapshot fallback when variant has been deleted since fulfillment — returns null, render a placeholder). |
525
525
  | `Shipment` | `Shipment` | Full shipment shape — provider, tracking number + URL, label URL, status, ETA, recipient address, packages, items, full event timeline. Spread on the tracking page. |
526
526
  | `ShipmentBasic` | `Shipment` | Lightweight shipment shape — id, tracking number + URL, status, dates. No items / events / address. Use for order summary cards or on the public `shipmentByTrackingNumber` query. |
527
527
 
@@ -547,7 +547,7 @@ full executable body of each operation.
547
547
 
548
548
  | Fragment | On Type | Description |
549
549
  | --- | --- | --- |
550
- | `ShippingCarrier` | `ShippingCarrier` | Carrier offering the shipping method — id, display name, logo URL, internal service code. |
550
+ | `ShippingCarrier` | `ShippingCarrier` | Carrier offering the shipping method — id, display name, logo image (transformable), internal service code. |
551
551
  | `DeliveryEstimate` | `DeliveryEstimate` | Estimated delivery window — `minDays` to `maxDays` plus a human-readable description (e.g. "2-4 business days"). |
552
552
  | `FreeShippingProgress` | `FreeShippingProgress` | Free-shipping progress info — `qualifies` flag, current cart amount, threshold, remaining to qualify, percent progress, and a localized message ("Add 20 PLN more for free shipping"). Use to render a free-shipping progress bar in the cart drawer. |
553
553
  | `AvailableShippingMethod` | `AvailableShippingMethod` | One shipping method offered for the destination + cart — id, name, carrier, price, free-shipping progress, estimated delivery, sort order. `deliveryType` signals whether to render a pickup-point / locker picker (`HOME` vs `PICKUP_POINT` / `LOCKER`) so the storefront does not have to infer it from the method name. Spread on the checkout shipping step. |
@@ -556,7 +556,7 @@ full executable body of each operation.
556
556
 
557
557
  | Fragment | On Type | Description |
558
558
  | --- | --- | --- |
559
- | `AttributeSwatch` | `AttributeSwatch` | Visual swatch for an attribute filter value — color hex (for color filters) or image URL (for pattern/material swatches). |
559
+ | `AttributeSwatch` | `AttributeSwatch` | Visual swatch for an attribute filter value — color hex (for color filters) or image (for pattern/material swatches). |
560
560
  | `AttributeRangeBounds` | `AttributeRangeBounds` | Numeric range bounds for slider-style filters — min, max, currency code (when relevant). |
561
561
  | `AttributeFilterValue` | `AttributeFilterValue` | One discrete value in a filterable attribute (e.g. "Red" for the Color filter). Includes `productCount` in the current listing context (for "(12)" badges next to filter values), optional swatch and price modifier. |
562
562
  | `AttributeDefinition` | `AttributeDefinition` | Filterable attribute exposed on the storefront — name, type (SELECT / CHECKBOX / SLIDER / etc.), filterability flags, display order, and either discrete `filterValues[]` or numeric `rangeBounds`. Spread inside `availableFilters.attributes[]`. |