@doswiftly/storefront-sdk 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/CHANGELOG.md +976 -0
- package/README.md +47 -4
- package/dist/core/auth/auth-client.d.ts +39 -3
- package/dist/core/auth/auth-client.d.ts.map +1 -1
- package/dist/core/auth/auth-client.js +51 -3
- package/dist/core/auth/cookie-config.d.ts +52 -3
- package/dist/core/auth/cookie-config.d.ts.map +1 -1
- package/dist/core/auth/cookie-config.js +60 -6
- package/dist/core/auth/handlers.d.ts +46 -0
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +9 -2
- package/dist/core/auth/session-events.d.ts +38 -0
- package/dist/core/auth/session-events.d.ts.map +1 -0
- package/dist/core/auth/session-events.js +35 -0
- package/dist/core/cart/cart-recovery.d.ts +23 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -1
- package/dist/core/cart/cart-recovery.js +20 -3
- package/dist/core/cart/types.d.ts +2 -1
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +7 -1
- package/dist/core/client/create-client.d.ts.map +1 -1
- package/dist/core/client/create-client.js +7 -3
- package/dist/core/client/execute.d.ts +29 -3
- package/dist/core/client/execute.d.ts.map +1 -1
- package/dist/core/client/execute.js +174 -3
- package/dist/core/client/types.d.ts +50 -2
- package/dist/core/client/types.d.ts.map +1 -1
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/generated/operation-types.d.ts +838 -221
- package/dist/core/generated/operation-types.d.ts.map +1 -1
- package/dist/core/generated/operation-types.js +560 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +12 -2
- package/dist/core/middleware/session-retry.d.ts +47 -0
- package/dist/core/middleware/session-retry.d.ts.map +1 -0
- package/dist/core/middleware/session-retry.js +71 -0
- package/dist/core/operations/auth.d.ts.map +1 -1
- package/dist/core/operations/auth.js +1 -0
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +15 -11
- package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -1
- package/dist/react/components/PaymentInstrumentSection.js +4 -4
- package/dist/react/components/PaymentInstrumentTile.d.ts +7 -7
- package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -1
- package/dist/react/components/PaymentInstrumentTile.js +4 -3
- package/dist/react/hooks/use-cart-manager.d.ts +133 -13
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +220 -16
- package/dist/react/hooks/use-login.d.ts.map +1 -1
- package/dist/react/hooks/use-login.js +3 -3
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
- package/dist/react/hooks/use-refresh-token.js +6 -4
- package/dist/react/hooks/use-session-expired.d.ts +16 -0
- package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
- package/dist/react/hooks/use-session-expired.js +26 -0
- package/dist/react/hooks/use-session-refresh.d.ts +32 -0
- package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
- package/dist/react/hooks/use-session-refresh.js +147 -0
- package/dist/react/index.d.ts +5 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/providers/cart-manager-provider.d.ts +50 -0
- package/dist/react/providers/cart-manager-provider.d.ts.map +1 -0
- package/dist/react/providers/cart-manager-provider.js +59 -0
- package/dist/react/providers/storefront-client-provider.d.ts +10 -1
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-client-provider.js +38 -3
- package/dist/react/providers/storefront-provider.d.ts +51 -3
- package/dist/react/providers/storefront-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-provider.js +22 -5
- package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
- package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
- package/dist/react/server/create-storefront-auth-route.js +239 -0
- package/dist/react/server/get-initial-auth.d.ts +57 -0
- package/dist/react/server/get-initial-auth.d.ts.map +1 -0
- package/dist/react/server/get-initial-auth.js +55 -0
- package/dist/react/server/index.d.ts +3 -0
- package/dist/react/server/index.d.ts.map +1 -1
- package/dist/react/server/index.js +6 -0
- package/dist/react/stores/auth.store.d.ts +46 -2
- package/dist/react/stores/auth.store.d.ts.map +1 -1
- package/dist/react/stores/auth.store.js +19 -7
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,981 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 18.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ff03fd3: Add `<CartManagerProvider>` + `useCartManagerContext()` so a single cart manager can be shared across a checkout tree, plus optional lifecycle callbacks on `useCartManager` and the provider.
|
|
8
|
+
|
|
9
|
+
**Why** — `useCartManager` keeps per-mount state (loading status, recovery coordinator, cart-expired listeners). Calling it in several components creates independent managers, so a checkout that wants one loading indicator, one recovery queue, and one cart-expired subscription had to hand-roll a React Context wrapper. This ships that wrapper as a first-class, opt-in provider, matching the provider-first pattern already used by `<StorefrontProvider>`, `<CurrencyProvider>`, and `<CartProvider>`.
|
|
10
|
+
|
|
11
|
+
**Additive (backward-compatible)**:
|
|
12
|
+
1. `<CartManagerProvider>` creates one `useCartManager` instance and exposes it through Context; `useCartManagerContext()` reads it (throws when used outside the provider). The provider must be rendered inside `<StorefrontProvider>`.
|
|
13
|
+
2. New optional lifecycle callbacks, accepted both by `useCartManager(options)` and as `<CartManagerProvider>` props:
|
|
14
|
+
- `onMutationStart(operation)` — fired when a mutation starts.
|
|
15
|
+
- `onMutationSuccess(operation)` — fired after it resolves.
|
|
16
|
+
- `onMutationError(operation, error)` — fired with a buyer-surfaceable error (its message comes from the backend). Cart expiry and session loss are delivered through the dedicated `onExpired` / session-expired channels and do NOT trigger `onMutationError`, so `toast.error(error.message)` is safe to wire.
|
|
17
|
+
|
|
18
|
+
A transient missing-cart error that the manager transparently recovers from resolves as success. Callbacks are invoked defensively — a throwing callback never rejects the underlying mutation (it is logged via `console.warn`).
|
|
19
|
+
|
|
20
|
+
3. New exported types: `CartManagerProviderProps`, `CartManagerLifecycleCallbacks`, `UseCartManagerOptions`.
|
|
21
|
+
|
|
22
|
+
**Usage example**:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
"use client";
|
|
26
|
+
import { useRouter } from "next/navigation";
|
|
27
|
+
import { toast } from "sonner";
|
|
28
|
+
import {
|
|
29
|
+
CartManagerProvider,
|
|
30
|
+
useCartManagerContext,
|
|
31
|
+
} from "@doswiftly/storefront-sdk/react";
|
|
32
|
+
|
|
33
|
+
function Checkout({ initialCartId }: { initialCartId: string | null }) {
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
return (
|
|
36
|
+
<CartManagerProvider
|
|
37
|
+
initialCartId={initialCartId}
|
|
38
|
+
onMutationSuccess={() => router.refresh()}
|
|
39
|
+
onMutationError={(operation, error) => toast.error(error.message)}
|
|
40
|
+
>
|
|
41
|
+
<CheckoutForm />
|
|
42
|
+
</CartManagerProvider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CheckoutForm() {
|
|
47
|
+
// one shared manager for the whole form
|
|
48
|
+
const { addItem, complete, status } = useCartManagerContext();
|
|
49
|
+
// ...
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Migration checklist** — none required; everything is opt-in.
|
|
54
|
+
- [ ] Optional: replace a hand-rolled `useCartManager` Context wrapper with `<CartManagerProvider>` + `useCartManagerContext()`.
|
|
55
|
+
- [ ] Optional: move per-call toast / refresh side-effects into `onMutationSuccess` / `onMutationError` on the provider.
|
|
56
|
+
- [ ] Keep calling `useCartManager()` directly where you want independent managers (e.g. multi-cart B2B, an admin "view this cart" panel).
|
|
57
|
+
|
|
58
|
+
**Standards reference** — provider-first sharing of a stateful instance with listeners and async initialisation is the established React-ecosystem pattern (`QueryClientProvider`, Stripe `<Elements>`), and mirrors the provider-first cart APIs of leading commerce SDKs.
|
|
59
|
+
|
|
60
|
+
## 18.0.0
|
|
61
|
+
|
|
62
|
+
### Major Changes
|
|
63
|
+
|
|
64
|
+
- 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.
|
|
65
|
+
|
|
66
|
+
**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.
|
|
67
|
+
|
|
68
|
+
**Schema diff (9 fields)**:
|
|
69
|
+
|
|
70
|
+
| Type | Before | After |
|
|
71
|
+
| ------------------------- | ----------------------- | ------------------- |
|
|
72
|
+
| `ReturnLineItem` | `imageUrl: String` | `image: Image` |
|
|
73
|
+
| `Brand` | `logo: String` | `logo: Image` |
|
|
74
|
+
| `BrandFilterValue` | `logo: String` | `logo: Image` |
|
|
75
|
+
| `ShippingCarrier` | `logoUrl: String` | `logo: Image` |
|
|
76
|
+
| `TierBenefit` | `icon: String` | `icon: Image` |
|
|
77
|
+
| `ProductReview` | `images: [String]` | `images: [Image!]!` |
|
|
78
|
+
| `AttributeSwatch` | `imageUrl: String` | `image: Image` |
|
|
79
|
+
| `PaymentMethod` | `icon: String` | `icon: Image` |
|
|
80
|
+
| `PaymentMethodInstrument` | `brandImageUrl: String` | `brandImage: Image` |
|
|
81
|
+
|
|
82
|
+
**Migration — pick a thumbnail fragment and replace selections**:
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
```diff
|
|
87
|
+
fragment ReturnItem on ReturnItem {
|
|
88
|
+
id
|
|
89
|
+
variantTitle
|
|
90
|
+
- imageUrl
|
|
91
|
+
+ image {
|
|
92
|
+
+ ...ImageThumbnail
|
|
93
|
+
+ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fragment ShippingCarrier on ShippingCarrier {
|
|
97
|
+
id
|
|
98
|
+
name
|
|
99
|
+
- logoUrl
|
|
100
|
+
+ logo {
|
|
101
|
+
+ ...ImageThumbnail
|
|
102
|
+
+ }
|
|
103
|
+
serviceCode
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fragment AttributeSwatch on AttributeSwatch {
|
|
107
|
+
colorHex
|
|
108
|
+
- imageUrl
|
|
109
|
+
+ image {
|
|
110
|
+
+ ...ImageThumbnail
|
|
111
|
+
+ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fragment ProductReview on ProductReview {
|
|
115
|
+
id
|
|
116
|
+
rating
|
|
117
|
+
- images
|
|
118
|
+
+ images {
|
|
119
|
+
+ ...ImageThumbnail
|
|
120
|
+
+ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fragment LoyaltyTier on LoyaltyTier {
|
|
124
|
+
id
|
|
125
|
+
name
|
|
126
|
+
customBenefits {
|
|
127
|
+
name
|
|
128
|
+
description
|
|
129
|
+
- icon
|
|
130
|
+
+ icon {
|
|
131
|
+
+ ...ImageThumbnail
|
|
132
|
+
+ }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fragment PaymentMethod on PaymentMethod {
|
|
137
|
+
id
|
|
138
|
+
name
|
|
139
|
+
type
|
|
140
|
+
- icon
|
|
141
|
+
+ icon {
|
|
142
|
+
+ ...ImageThumbnail
|
|
143
|
+
+ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fragment PaymentMethodInstrument on PaymentMethodInstrument {
|
|
147
|
+
providerCode
|
|
148
|
+
instrumentCode
|
|
149
|
+
displayName
|
|
150
|
+
- brandImageUrl
|
|
151
|
+
+ brandImage {
|
|
152
|
+
+ ...ImageThumbnail
|
|
153
|
+
+ }
|
|
154
|
+
enabled
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Brand (used inside MenuItem.resource union and as Product.brand)
|
|
158
|
+
{
|
|
159
|
+
brand {
|
|
160
|
+
id
|
|
161
|
+
handle
|
|
162
|
+
name
|
|
163
|
+
- logo
|
|
164
|
+
+ logo {
|
|
165
|
+
+ ...ImageThumbnail
|
|
166
|
+
+ }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# BrandFilterValue (used inside AvailableFilters.brands)
|
|
171
|
+
{
|
|
172
|
+
brands {
|
|
173
|
+
id
|
|
174
|
+
handle
|
|
175
|
+
name
|
|
176
|
+
- logo
|
|
177
|
+
+ logo {
|
|
178
|
+
+ ...ImageThumbnail
|
|
179
|
+
+ }
|
|
180
|
+
productCount
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Field renames** (additional to type changes):
|
|
186
|
+
- `ShippingCarrier.logoUrl` → `ShippingCarrier.logo`
|
|
187
|
+
- `AttributeSwatch.imageUrl` → `AttributeSwatch.image`
|
|
188
|
+
- `ReturnLineItem.imageUrl` → `ReturnLineItem.image`
|
|
189
|
+
- `PaymentMethodInstrument.brandImageUrl` → `PaymentMethodInstrument.brandImage`
|
|
190
|
+
|
|
191
|
+
Rename + type change happen in the same release; you have to update both the field name and the selection set.
|
|
192
|
+
|
|
193
|
+
**Inline transform alternative**: if you do not want to use `ImageThumbnail`, you can request the URL with an explicit transform — the result is identical:
|
|
194
|
+
|
|
195
|
+
```graphql
|
|
196
|
+
{
|
|
197
|
+
carrier {
|
|
198
|
+
logo {
|
|
199
|
+
url(transform: { maxWidth: 64, preferredContentType: WEBP })
|
|
200
|
+
altText
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Migration checklist**:
|
|
207
|
+
- [ ] Update every fragment listed above so the selection set requests `url`/`altText`/etc. instead of treating the field as a scalar.
|
|
208
|
+
- [ ] Rename selections for the four fields that changed name (`logoUrl`/`imageUrl`/`brandImageUrl`).
|
|
209
|
+
- [ ] Re-run codegen so your generated TypedDocumentNode bindings reflect the new shape.
|
|
210
|
+
- [ ] 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.
|
|
211
|
+
- [ ] `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.
|
|
212
|
+
|
|
213
|
+
**No backend-input breakage**: `ReviewCreateInput.images` still takes `[String]` URLs from the customer at submission time. Only the response field changed shape.
|
|
214
|
+
|
|
215
|
+
- e28e902: Payment schema naming overhaul — one coherent provider / method / instrument model.
|
|
216
|
+
|
|
217
|
+
**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).
|
|
218
|
+
|
|
219
|
+
**Breaking**:
|
|
220
|
+
1. Enum `ProviderCode` → `PaymentProvider` (values unchanged: `PAYU`, `PRZELEWY24`, `STRIPE`, ...).
|
|
221
|
+
2. Object `PaymentMethodInstrument` → `PaymentInstrument`; field `instrumentCode` → `code`, field `providerCode` → `provider`.
|
|
222
|
+
3. Enums `PaymentMethodInstrumentType` → `PaymentInstrumentType`, `PaymentMethodInstrumentDisplayHint` → `PaymentInstrumentDisplayHint`.
|
|
223
|
+
4. `Cart.selectedPaymentInstrumentCode` → `Cart.selectedPaymentInstrument`.
|
|
224
|
+
5. `CartSelectPaymentMethodInput`: `preferredProviderCode` → `preferredProvider`, `preferredInstrumentCode` → `preferredInstrument`.
|
|
225
|
+
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.)
|
|
226
|
+
|
|
227
|
+
**Usage example**:
|
|
228
|
+
|
|
229
|
+
```graphql
|
|
230
|
+
# before
|
|
231
|
+
fragment Instrument on PaymentMethodInstrument {
|
|
232
|
+
providerCode
|
|
233
|
+
instrumentCode
|
|
234
|
+
displayName
|
|
235
|
+
}
|
|
236
|
+
mutation Select($input: CartSelectPaymentMethodInput!) {
|
|
237
|
+
cartSelectPaymentMethod(input: $input) {
|
|
238
|
+
cart {
|
|
239
|
+
selectedPaymentInstrumentCode
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
# $input: { cartId, methodType: BLIK, preferredProviderCode: "payu", preferredInstrumentCode: "blik" }
|
|
244
|
+
|
|
245
|
+
# after
|
|
246
|
+
fragment Instrument on PaymentInstrument {
|
|
247
|
+
provider
|
|
248
|
+
code
|
|
249
|
+
displayName
|
|
250
|
+
}
|
|
251
|
+
mutation Select($input: CartSelectPaymentMethodInput!) {
|
|
252
|
+
cartSelectPaymentMethod(input: $input) {
|
|
253
|
+
cart {
|
|
254
|
+
selectedPaymentInstrument
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
# $input: { cartId, methodType: BLIK, preferredProvider: PAYU, preferredInstrument: "blik" }
|
|
259
|
+
# ^ preferredProvider is the PaymentProvider enum (UPPERCASE) — read it from
|
|
260
|
+
# `instrument.provider` / `method.preferredProvider` and pass it back verbatim.
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// SDK types — before
|
|
265
|
+
import type {
|
|
266
|
+
PaymentMethodInstrument,
|
|
267
|
+
ProviderCode,
|
|
268
|
+
} from "@doswiftly/storefront-sdk";
|
|
269
|
+
const code = instrument.instrumentCode;
|
|
270
|
+
const who = instrument.providerCode;
|
|
271
|
+
|
|
272
|
+
// after
|
|
273
|
+
import type {
|
|
274
|
+
PaymentInstrument,
|
|
275
|
+
PaymentProvider,
|
|
276
|
+
} from "@doswiftly/storefront-sdk";
|
|
277
|
+
const code = instrument.code;
|
|
278
|
+
const who = instrument.provider;
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The pre-built `<PaymentInstrumentSection>` / `<PaymentInstrumentTile>` component props (`selectedInstrumentCode`, `onSelectInstrument`) are unchanged — only the instrument data shape (`.code`) changed.
|
|
282
|
+
|
|
283
|
+
**Migration checklist for existing storefronts**:
|
|
284
|
+
- [ ] Rename type references: `PaymentMethodInstrument` → `PaymentInstrument`, `ProviderCode` → `PaymentProvider`, `PaymentMethodInstrumentType`/`PaymentMethodInstrumentDisplayHint` → `PaymentInstrumentType`/`PaymentInstrumentDisplayHint`
|
|
285
|
+
- [ ] Field access: `instrument.instrumentCode` → `instrument.code`, `instrument.providerCode` → `instrument.provider`
|
|
286
|
+
- [ ] Cart selection: `cart.selectedPaymentInstrumentCode` → `cart.selectedPaymentInstrument`
|
|
287
|
+
- [ ] Mutation input: `preferredProviderCode` → `preferredProvider`, `preferredInstrumentCode` → `preferredInstrument`
|
|
288
|
+
- [ ] 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.
|
|
289
|
+
- [ ] If you hand-write GraphQL operations: update fragments + re-run codegen against the published `schema.graphql`
|
|
290
|
+
|
|
291
|
+
- 159184c: `ShipmentItem.imageUrl: String` replaced with `ShipmentItem.image: Image`.
|
|
292
|
+
|
|
293
|
+
**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 }) }`.
|
|
294
|
+
|
|
295
|
+
**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.
|
|
296
|
+
|
|
297
|
+
**Migration**:
|
|
298
|
+
|
|
299
|
+
```diff
|
|
300
|
+
fragment ShipmentItem on ShipmentItem {
|
|
301
|
+
id
|
|
302
|
+
title
|
|
303
|
+
variantTitle
|
|
304
|
+
sku
|
|
305
|
+
quantity
|
|
306
|
+
- imageUrl
|
|
307
|
+
+ image {
|
|
308
|
+
+ ...ImageThumbnail
|
|
309
|
+
+ }
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Or, if you prefer to inline the transform without using the `ImageThumbnail` fragment:
|
|
314
|
+
|
|
315
|
+
```diff
|
|
316
|
+
fragment ShipmentItem on ShipmentItem {
|
|
317
|
+
id
|
|
318
|
+
title
|
|
319
|
+
variantTitle
|
|
320
|
+
sku
|
|
321
|
+
quantity
|
|
322
|
+
- imageUrl
|
|
323
|
+
+ image {
|
|
324
|
+
+ url(transform: { maxWidth: 200, preferredContentType: WEBP })
|
|
325
|
+
+ altText
|
|
326
|
+
+ }
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Migration checklist**:
|
|
331
|
+
- [ ] Replace `imageUrl` selections in your `ShipmentItem` fragments / inline queries with `image { ...ImageThumbnail }` (or inline transform).
|
|
332
|
+
- [ ] Handle the `image: null` case in your ShipmentItem rendering (variant deleted after fulfillment — render a placeholder).
|
|
333
|
+
- [ ] Re-run codegen so your TypedDocumentNode bindings reflect the new shape.
|
|
334
|
+
- [ ] 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`.
|
|
335
|
+
|
|
336
|
+
- cc7edbe: SDK-BFF customer authentication — same-origin route handlers + automatic session refresh.
|
|
337
|
+
|
|
338
|
+
**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.
|
|
339
|
+
|
|
340
|
+
**New**
|
|
341
|
+
- `createStorefrontAuthRoute({ apiUrl, shopSlug, isTrustedOrigin? })` (from `@doswiftly/storefront-sdk/react/server`) generates the `login` / `refresh` / `logout` / `whoami` route handlers. Mount once:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// app/api/auth/[action]/route.ts
|
|
345
|
+
import {
|
|
346
|
+
createStorefrontAuthRoute,
|
|
347
|
+
trustedForwardedHostValidator,
|
|
348
|
+
} from "@doswiftly/storefront-sdk/react/server";
|
|
349
|
+
|
|
350
|
+
export const { GET, POST } = createStorefrontAuthRoute({
|
|
351
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
352
|
+
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
353
|
+
isTrustedOrigin: trustedForwardedHostValidator, // when running behind a reverse proxy
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
- `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:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// app/layout.tsx (Server Component)
|
|
361
|
+
const { isAuthenticated, accessToken, expiresAt } = await getInitialAuth();
|
|
362
|
+
return (
|
|
363
|
+
<StorefrontProvider
|
|
364
|
+
initialIsAuthenticated={isAuthenticated}
|
|
365
|
+
initialAccessToken={accessToken}
|
|
366
|
+
initialExpiresAt={expiresAt}
|
|
367
|
+
/* ...config, shopData */
|
|
368
|
+
>
|
|
369
|
+
{children}
|
|
370
|
+
</StorefrontProvider>
|
|
371
|
+
);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- `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.
|
|
375
|
+
|
|
376
|
+
**Breaking**
|
|
377
|
+
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.
|
|
378
|
+
2. `useSessionRefresh` no longer accepts an `authBasePath` option — the base path comes from the provider-configured auth client.
|
|
379
|
+
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).
|
|
380
|
+
|
|
381
|
+
**Migration checklist**
|
|
382
|
+
- [ ] Add `app/api/auth/[action]/route.ts` exporting `createStorefrontAuthRoute(...)`.
|
|
383
|
+
- [ ] Seed the provider from `getInitialAuth()` in your root layout (Server Component).
|
|
384
|
+
- [ ] Remove any `authBasePath` argument passed to `useSessionRefresh`.
|
|
385
|
+
- [ ] Subscribe with `useSessionExpired(cb)` to react globally (notice + redirect to sign-in).
|
|
386
|
+
- [ ] Behind a reverse proxy that rewrites `Host`, pass `isTrustedOrigin: trustedForwardedHostValidator`.
|
|
387
|
+
|
|
388
|
+
**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).
|
|
389
|
+
|
|
390
|
+
`@doswiftly/storefront-operations` is version-synced with this release (linked pair); it has no operation changes in this entry.
|
|
391
|
+
|
|
392
|
+
### Minor Changes
|
|
393
|
+
|
|
394
|
+
- 9969bdd: Keep customer sessions alive automatically — proactive refresh + session-expiry awareness.
|
|
395
|
+
|
|
396
|
+
**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.
|
|
397
|
+
|
|
398
|
+
**Additive (backward-compatible)**:
|
|
399
|
+
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.
|
|
400
|
+
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.
|
|
401
|
+
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.
|
|
402
|
+
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.
|
|
403
|
+
|
|
404
|
+
**Usage**:
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
// works out of the box — proactive refresh is on by default in the browser
|
|
408
|
+
// (nothing to pass; opt out with autoRefresh={false})
|
|
409
|
+
<StorefrontProvider config={config} shopData={shopData}>
|
|
410
|
+
{children}
|
|
411
|
+
</StorefrontProvider>;
|
|
412
|
+
|
|
413
|
+
// anywhere near the root — react globally when a session could not be saved
|
|
414
|
+
useSessionExpired(() => {
|
|
415
|
+
toast("Your session expired — please sign in again.");
|
|
416
|
+
router.push("/login");
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**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.
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
- 5525fd4: `createStorefrontClient({ debug })` now accepts a tagged union — `boolean | 'verbose' | DebugOptions` — and a `'verbose'` mode dumps the full request and response with `Authorization` / `customerAccessToken` redacted. Mutation `userErrors[]` are surfaced on the response log by default.
|
|
427
|
+
|
|
428
|
+
**Why** — until now `debug: true` logged only `operationName + variables` on the request and `status + hasErrors` on the response. To see the actual GraphQL query, the response body, the populated `userErrors[]`, headers, or timing, you had to open DevTools → Network → filter by `graphql` → click the request → scroll Response. With dozens of requests per checkout session that workflow is painful. This release surfaces the same information inline in the console — opt-in, per-dimension, with sensitive credentials redacted unconditionally.
|
|
429
|
+
|
|
430
|
+
**Additive (backward-compatible)**:
|
|
431
|
+
1. `debug: true` continues to produce the exact same output as v17.1.0 — minimal request/response meta. Existing consumers see no change.
|
|
432
|
+
2. `debug: 'verbose'` enables every dimension:
|
|
433
|
+
- Full GraphQL query document on the request.
|
|
434
|
+
- Variables on the request (already logged in `true` mode).
|
|
435
|
+
- Request headers on the request, redacted (see below).
|
|
436
|
+
- Full response body (`data` + `errors` + `extensions`) on the response.
|
|
437
|
+
- Response headers on the response, redacted.
|
|
438
|
+
- Flattened `userErrors[]` from mutation payloads, on the response.
|
|
439
|
+
- `durationMs` (request start → response received) on the response.
|
|
440
|
+
3. `debug: DebugOptions` enables individual dimensions:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
interface DebugOptions {
|
|
444
|
+
request?: boolean; // full query string (default false)
|
|
445
|
+
response?: boolean; // full data + errors + extensions (default false)
|
|
446
|
+
headers?: boolean; // request + response headers (default false)
|
|
447
|
+
timing?: boolean; // durationMs (default false)
|
|
448
|
+
userErrors?: boolean; // flat userErrors[] from mutations (default true)
|
|
449
|
+
log?: (event: DebugEvent) => void; // custom sink (default console.log)
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`DebugEvent` is a stable contract:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
interface DebugEvent {
|
|
457
|
+
phase: "request" | "response";
|
|
458
|
+
operationName: string | undefined;
|
|
459
|
+
data: Record<string, unknown>;
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
4. Environment variable opt-in: `DOSWIFTLY_SDK_DEBUG=verbose` (or `true` / `1` / `minimal`) activates the chosen preset when `debug` is omitted in code. The env var is **ignored** when `NODE_ENV=production` to prevent accidental customer-data logging from a misconfigured environment — explicit programmatic `debug` is still honoured (operator-driven traces).
|
|
464
|
+
|
|
465
|
+
**Deep nested payloads render in full** — the default logger emits the header line via `console.log` and the data payload via `console.dir(data, { depth: null, colors: false })`. Node's default `util.inspect` depth is 2, which truncates `data: { cart: { shippingAddress: [Object] } }` exactly when verbose mode is supposed to surface it. `console.dir(..., { depth: null })` is the cross-runtime escape hatch — Node honours the option, browser DevTools ignores it and renders an interactive drill-down. `colors: false` keeps log files and CI output grep-friendly. If you pipe through a custom `log` sink the default sink is bypassed entirely — your serializer (`pino`, `winston`, JSON.stringify) decides depth.
|
|
466
|
+
|
|
467
|
+
**Redaction** — unconditional whenever headers are logged, regardless of mode:
|
|
468
|
+
- `Authorization: Bearer <token>` → `Authorization: Bearer ***<last4>`
|
|
469
|
+
- `Cookie: customerAccessToken=<value>; …` → `Cookie: customerAccessToken=***<last4>; …`
|
|
470
|
+
- `Set-Cookie: customerAccessToken=<value>; …` → same masking on response headers.
|
|
471
|
+
|
|
472
|
+
**Usage example** — point a verbose log at a structured logger:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { createStorefrontClient } from "@doswiftly/storefront-sdk";
|
|
476
|
+
import pino from "pino";
|
|
477
|
+
|
|
478
|
+
const logger = pino({ name: "storefront" });
|
|
479
|
+
|
|
480
|
+
const client = createStorefrontClient({
|
|
481
|
+
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
482
|
+
shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
483
|
+
debug: {
|
|
484
|
+
request: true,
|
|
485
|
+
response: true,
|
|
486
|
+
headers: true,
|
|
487
|
+
timing: true,
|
|
488
|
+
userErrors: true,
|
|
489
|
+
log: (event) =>
|
|
490
|
+
logger[event.phase === "request" ? "debug" : "info"]({
|
|
491
|
+
op: event.operationName,
|
|
492
|
+
...event.data,
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Or simply `debug: 'verbose'` to dump to `console.log` during development:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const client = createStorefrontClient({ apiUrl, shopSlug, debug: "verbose" });
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Or set `DOSWIFTLY_SDK_DEBUG=verbose` in `.env.local` and leave `debug` out of code entirely.
|
|
505
|
+
|
|
506
|
+
**Migration checklist** — none required. `debug: true` and `debug: false` (and omitting `debug`) behave exactly as before. Opt into the new modes when you need them.
|
|
507
|
+
|
|
508
|
+
**Standards reference** — the per-dimension shape and `log` sink hook align with the conventions used by Stripe SDK (`{ logLevel, log }`) and Apollo Client's `loggerLink`. The credential redaction matches the OWASP cheat-sheet guidance for logging requests behind an auth boundary.
|
|
509
|
+
|
|
510
|
+
- 5525fd4: Schema enums are now exported as runtime values **and** TypeScript types — usable with `z.enum(...)`, `Object.values(...)`, `Object.keys(...)`, and any other runtime validator.
|
|
511
|
+
|
|
512
|
+
**Why** — until now enums like `PaymentMethodType`, `ProductTypeEnum`, `CountryCode`, `CurrencyCode`, `DeliveryType`, `CartWarningCode`, `OrderPaymentStatus` (and 11 more) were emitted as type-only union literals: `type PaymentMethodType = 'CARD' | 'BLIK' | ...`. They compiled away to nothing, so you couldn't pipe them into `z.enum(...)`, iterate them in `Object.values(...)`, or expose them to a form validator without hand-maintaining a parallel list. Every SDK bump risked silent drift between the SDK's union and your local copy. This release fixes that.
|
|
513
|
+
|
|
514
|
+
**Additive (backward-compatible)**:
|
|
515
|
+
|
|
516
|
+
For every schema enum, the generated types now emit a pair under the same identifier:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
// runtime value
|
|
520
|
+
export const PaymentMethodType = {
|
|
521
|
+
BankTransfer: "BANK_TRANSFER",
|
|
522
|
+
Blik: "BLIK",
|
|
523
|
+
Card: "CARD",
|
|
524
|
+
CashOnDelivery: "CASH_ON_DELIVERY",
|
|
525
|
+
Other: "OTHER",
|
|
526
|
+
} as const;
|
|
527
|
+
|
|
528
|
+
// type alias (same shape as before — `'BANK_TRANSFER' | 'BLIK' | …`)
|
|
529
|
+
export type PaymentMethodType =
|
|
530
|
+
(typeof PaymentMethodType)[keyof typeof PaymentMethodType];
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
- `import type { PaymentMethodType }` keeps working — TypeScript resolves the alias as before.
|
|
534
|
+
- `import { PaymentMethodType }` now also brings in the const object.
|
|
535
|
+
- `Object.values(PaymentMethodType)` returns the schema string values (`'BANK_TRANSFER'`, `'BLIK'`, …).
|
|
536
|
+
|
|
537
|
+
Enum values use `SCREAMING_SNAKE_CASE` (matching the GraphQL schema), keys use `PascalCase` (idiomatic for runtime accessors).
|
|
538
|
+
|
|
539
|
+
**Usage example** — Zod schema with runtime values:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { PaymentMethodType, DeliveryType } from "@doswiftly/storefront-sdk";
|
|
543
|
+
import { z } from "zod";
|
|
544
|
+
|
|
545
|
+
// Cast is needed because z.enum requires a non-empty tuple literal — the
|
|
546
|
+
// runtime values are a string-literal subtype of PaymentMethodType.
|
|
547
|
+
const checkoutSchema = z.object({
|
|
548
|
+
paymentMethod: z.enum(
|
|
549
|
+
Object.values(PaymentMethodType) as [
|
|
550
|
+
PaymentMethodType,
|
|
551
|
+
...PaymentMethodType[],
|
|
552
|
+
],
|
|
553
|
+
),
|
|
554
|
+
deliveryType: z.enum(
|
|
555
|
+
Object.values(DeliveryType) as [DeliveryType, ...DeliveryType[]],
|
|
556
|
+
),
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Or compare directly at runtime:
|
|
560
|
+
if (selected === PaymentMethodType.Blik) {
|
|
561
|
+
// …
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Enums included in this release: `PaymentMethodType`, `PaymentInitiationFlow`, `PaymentMethodInstrumentDisplayHint`, `PaymentMethodInstrumentType`, `PaymentMethodUnavailableReason`, `DeliveryType`, `ProductTypeEnum`, `CurrencyCode`, `CountryCode`, `LanguageCode`, `WeightUnit`, `CartWarningCode`, `AttributeType`, `AttributeFillingMode`, `AttributeBillingMode`, `AttributeOptionSurchargeType`, `StorefrontOrderStatus`, `OrderPaymentStatus`, `OrderFulfillmentStatus`, `DiscountApplicationType`, `DiscountErrorCode`, `ProviderCode`.
|
|
566
|
+
|
|
567
|
+
`PaymentErrorCode` stays type-only — it is a hand-curated `userErrors[].code` contract from `createPayment`, not a GraphQL enum.
|
|
568
|
+
|
|
569
|
+
**Migration checklist** — optional cleanup for existing storefronts:
|
|
570
|
+
- [ ] Delete any locally-duplicated arrays of enum values (e.g. `const PAYMENT_METHOD_TYPES = ['CARD', 'BLIK', ...] as const`) and replace with `Object.values(PaymentMethodType)`.
|
|
571
|
+
- [ ] Replace ad-hoc `z.string()` validators on enum fields with `z.enum(Object.values(X) as [X, ...X[]])`.
|
|
572
|
+
- [ ] If you imported with `import type { X }` you do not have to change anything — the type alias still resolves identically.
|
|
573
|
+
|
|
574
|
+
**Standards reference** — this is the same emit pattern produced by `@graphql-codegen/typescript`'s `enumsAsConst: true`. Bundle size impact is negligible (each enum is a plain object with string-literal values, tree-shakeable at the consumer); SDK runtime dependencies remain at zero.
|
|
575
|
+
|
|
576
|
+
- 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`.
|
|
577
|
+
|
|
578
|
+
**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.
|
|
579
|
+
|
|
580
|
+
**Additive (backward-compatible)**:
|
|
581
|
+
|
|
582
|
+
Two new runtime values exposed by the const + type alias (per `enumsAsConst` codegen):
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { PaymentMethodType } from "@doswiftly/storefront-sdk";
|
|
586
|
+
|
|
587
|
+
PaymentMethodType.Wallet; // 'WALLET'
|
|
588
|
+
PaymentMethodType.Installment; // 'INSTALLMENT'
|
|
589
|
+
|
|
590
|
+
Object.values(PaymentMethodType);
|
|
591
|
+
// ['BANK_TRANSFER', 'BLIK', 'CARD', 'CASH_ON_DELIVERY', 'INSTALLMENT', 'OTHER', 'WALLET']
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
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.
|
|
595
|
+
|
|
596
|
+
**Usage example** — render brand-correct iconography for wallet checkouts:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { PaymentMethodType } from "@doswiftly/storefront-sdk";
|
|
600
|
+
|
|
601
|
+
function PaymentMethodIcon({ type }: { type: PaymentMethodType }) {
|
|
602
|
+
switch (type) {
|
|
603
|
+
case PaymentMethodType.Card:
|
|
604
|
+
return <CardIcon />;
|
|
605
|
+
case PaymentMethodType.Blik:
|
|
606
|
+
return <BlikIcon />;
|
|
607
|
+
case PaymentMethodType.BankTransfer:
|
|
608
|
+
return <BankIcon />;
|
|
609
|
+
case PaymentMethodType.Wallet:
|
|
610
|
+
return <WalletIcon />; // NEW — Apple Pay / Google Pay
|
|
611
|
+
case PaymentMethodType.Installment:
|
|
612
|
+
return <InstallmentIcon />; // NEW — Klarna / PayPo / Twisto
|
|
613
|
+
case PaymentMethodType.CashOnDelivery:
|
|
614
|
+
return <CashIcon />;
|
|
615
|
+
case PaymentMethodType.Other:
|
|
616
|
+
return <GenericPaymentIcon />;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Migration checklist** — none required for storefronts that already routed `OTHER` to a generic icon:
|
|
622
|
+
- [ ] (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.
|
|
623
|
+
- [ ] (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.
|
|
624
|
+
|
|
625
|
+
**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.
|
|
626
|
+
|
|
627
|
+
- 78bd561: `<StorefrontProvider>` now accepts an `initialAccessToken` prop for server-side JWT seed, plus `authStore.setAuth` accepts a nullable customer.
|
|
628
|
+
|
|
629
|
+
**Why** — when your storefront has the raw access token server-side (httpOnly cookie value read from a Server Component, SSO redirect parameter, magic link callback, dev-seed env var), previously there was no clean way to inject it into the auth store on the first render. You had to write a `<DevAuthHydration>` workaround with `useEffect`, accept an unnecessary round-trip to `/api/auth/whoami` before `authMiddleware` could add `Authorization: Bearer ...`, or drop down to `useAuthStoreApi().setState({ accessToken })` because `setAuth` required a non-null customer (which you don't have when all you've got is a raw JWT). This release closes that gap.
|
|
630
|
+
|
|
631
|
+
**Additive (backward-compatible)**:
|
|
632
|
+
1. New optional prop on `<StorefrontProvider>`:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
interface StorefrontProviderProps {
|
|
636
|
+
// existing props unchanged
|
|
637
|
+
initialIsAuthenticated?: boolean;
|
|
638
|
+
initialLanguage?: string;
|
|
639
|
+
|
|
640
|
+
// NEW
|
|
641
|
+
initialAccessToken?: string | null;
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
The value seeds the auth store on first mount, so the SDK's request interceptor attaches `Authorization: Bearer ...` to every outgoing GraphQL call from the very first request — no round-trip to `/api/auth/whoami` required to populate the token.
|
|
646
|
+
|
|
647
|
+
2. `isAuthenticated` defaults to `!!initialAccessToken` when `initialIsAuthenticated` is not provided (a raw token implies authenticated). Pass `initialIsAuthenticated={false}` explicitly to override in edge cases (opt-out flow, recovery banner holding a token without claiming auth state).
|
|
648
|
+
3. `AuthStore.setAuth` signature relaxed from `(customer: CustomerInfo, accessToken: string) => void` to `(customer: CustomerInfo | null, accessToken: string) => void`. The runtime already accepted `null`; this aligns the compile-time type with actual behavior (`AuthStore.customer` was already `CustomerInfo | null`). Use case: you have the token (from `initialAccessToken`, SSO callback, magic link) but the customer profile is fetched separately — `setAuth(null, token)` is now valid instead of bypassing the action with `setState`.
|
|
649
|
+
|
|
650
|
+
**Usage example**:
|
|
651
|
+
|
|
652
|
+
```tsx
|
|
653
|
+
// app/layout.tsx
|
|
654
|
+
import { cookies } from "next/headers";
|
|
655
|
+
import {
|
|
656
|
+
StorefrontProvider,
|
|
657
|
+
AUTH_COOKIE_NAME,
|
|
658
|
+
} from "@doswiftly/storefront-sdk";
|
|
659
|
+
|
|
660
|
+
export default async function RootLayout({ children }) {
|
|
661
|
+
const cookieStore = await cookies();
|
|
662
|
+
const initialAccessToken = cookieStore.get(AUTH_COOKIE_NAME)?.value ?? null;
|
|
663
|
+
|
|
664
|
+
return (
|
|
665
|
+
<StorefrontProvider
|
|
666
|
+
config={{ apiUrl, shopSlug }}
|
|
667
|
+
shopData={shop}
|
|
668
|
+
initialAccessToken={initialAccessToken}
|
|
669
|
+
>
|
|
670
|
+
{children}
|
|
671
|
+
</StorefrontProvider>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Security** — the seeded token lives **only in memory** (RAM). It is **never written to localStorage** (XSS hardening — same guarantee as for tokens set later via `useLogin()`, in place since v5.0.0). Hard refresh resets the in-memory token; your Server Component re-reads the httpOnly cookie and seeds the new render.
|
|
677
|
+
|
|
678
|
+
**Choosing between `initialIsAuthenticated` and `initialAccessToken`**:
|
|
679
|
+
|
|
680
|
+
| You know server-side | Pass | Round-trip to `/api/auth/whoami` |
|
|
681
|
+
| -------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ |
|
|
682
|
+
| _Whether_ the user is signed in (cookie present, value not readable) | `initialIsAuthenticated: boolean` | Required to fetch the token |
|
|
683
|
+
| The raw JWT itself (cookie value, SSO param, env seed) | `initialAccessToken: string \| null` | Not required — middleware works from the first request |
|
|
684
|
+
|
|
685
|
+
**Migration checklist** — none required. Existing consumers that pass only `initialIsAuthenticated` (or nothing at all) continue to work without changes. To opt into zero-round-trip auth init, read the cookie value (instead of just its presence) and pass it as `initialAccessToken`.
|
|
686
|
+
|
|
687
|
+
- 5525fd4: `useCartManager` now covers the full checkout lifecycle (9 new operations) and auto-clears the `cart-id` cookie after a successful `complete()` — buyers returning to `/checkout` after payment get a fresh empty cart instead of the converted one.
|
|
688
|
+
|
|
689
|
+
**Why** — until now `useCartManager` exposed only the shopping-cart surface (`addItem` / `updateItem` / `removeItem` / `setShippingAddress` / `updateBuyerIdentity` / `updateDiscountCodes` / `updateNote`). Anything past that point — selecting a shipping method, picking a payment method, applying a gift card, finalising the order — required dropping down to the raw `CartClient` and managing cart-id cookie cleanup manually. Two parallel APIs in one checkout form, plus a stale cookie that pointed at a completed cart after every successful order. This release closes both gaps in a single hook.
|
|
690
|
+
|
|
691
|
+
**Additive (backward-compatible)**:
|
|
692
|
+
1. New mutations on `UseCartManagerResult` — all wrapped in the same recovery runner that powers the existing surface, so a stale cart triggers either an auto-replay (`updateAttributes`) or a `cart-expired` event (`complete`, `selectShippingMethod`, `selectPaymentMethod`, `clearPaymentSelection`, `setBillingAddress`, `applyGiftCard`, `removeGiftCard`, `updateGiftCardRecipient`). `createPayment` operates on `orderId`, so it lives outside the recovery runner but still benefits from the shared `status` tracking.
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
interface UseCartManagerResult {
|
|
696
|
+
// existing — unchanged
|
|
697
|
+
addItem(lines: CartLineInput[]): Promise<CartMutationOutcome>;
|
|
698
|
+
updateItem(lines: CartLineUpdateInput[]): Promise<CartMutationOutcome>;
|
|
699
|
+
removeItem(lineIds: string[]): Promise<CartMutationOutcome>;
|
|
700
|
+
setShippingAddress(
|
|
701
|
+
address: CartAddressInput,
|
|
702
|
+
): Promise<CartMutationOutcome>;
|
|
703
|
+
updateBuyerIdentity(
|
|
704
|
+
buyerIdentity: CartBuyerIdentityInput,
|
|
705
|
+
): Promise<CartMutationOutcome>;
|
|
706
|
+
updateDiscountCodes(codes: string[]): Promise<CartMutationOutcome>;
|
|
707
|
+
updateNote(note: string): Promise<CartMutationOutcome>;
|
|
708
|
+
|
|
709
|
+
// NEW
|
|
710
|
+
updateAttributes(
|
|
711
|
+
attributes: CartAttributeInput[],
|
|
712
|
+
): Promise<CartMutationOutcome>;
|
|
713
|
+
setBillingAddress(
|
|
714
|
+
address: CartAddressInput,
|
|
715
|
+
): Promise<CartMutationOutcome>;
|
|
716
|
+
selectShippingMethod(
|
|
717
|
+
input: Omit<CartSelectShippingMethodInput, "cartId">,
|
|
718
|
+
): Promise<CartMutationOutcome>;
|
|
719
|
+
selectPaymentMethod(
|
|
720
|
+
input: Omit<CartSelectPaymentMethodInput, "cartId">,
|
|
721
|
+
): Promise<CartMutationOutcome>;
|
|
722
|
+
clearPaymentSelection(
|
|
723
|
+
input?: Omit<CartClearPaymentSelectionInput, "cartId">,
|
|
724
|
+
): Promise<CartMutationOutcome>;
|
|
725
|
+
applyGiftCard(
|
|
726
|
+
input: Omit<CartApplyGiftCardInput, "cartId">,
|
|
727
|
+
): Promise<CartMutationOutcome>;
|
|
728
|
+
removeGiftCard(
|
|
729
|
+
input: Omit<CartRemoveGiftCardInput, "cartId">,
|
|
730
|
+
): Promise<CartMutationOutcome>;
|
|
731
|
+
updateGiftCardRecipient(
|
|
732
|
+
input: Omit<CartUpdateGiftCardRecipientInput, "cartId">,
|
|
733
|
+
): Promise<CartMutationOutcome>;
|
|
734
|
+
complete(
|
|
735
|
+
input?: Omit<CartCompleteInput, "cartId">,
|
|
736
|
+
): Promise<CartCompleteOutcome>;
|
|
737
|
+
createPayment(input: PaymentCreateInput): Promise<PaymentSession>;
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Every method takes its input without `cartId` — the hook injects the current cookie value automatically, matching how `setShippingAddress(address)` and `updateDiscountCodes(codes)` already work.
|
|
742
|
+
|
|
743
|
+
2. `complete()` clears the `cart-id` cookie and resets `status` to `idle` after a successful call. A subsequent `addItem(...)` then creates a fresh cart automatically — the recovery runner reuses the same code path that already handles the empty-cookie case on a first-time add.
|
|
744
|
+
3. `status.operation` now includes the new operation names (`'complete'`, `'selectShippingMethod'`, `'selectPaymentMethod'`, `'clearPaymentSelection'`, `'setBillingAddress'`, `'updateAttributes'`, `'applyGiftCard'`, `'removeGiftCard'`, `'updateGiftCardRecipient'`, `'createPayment'`) — drive a single `<Spinner label={status.operation} />` across the whole checkout form.
|
|
745
|
+
|
|
746
|
+
**Usage example** — replace raw `CartClient` calls in your checkout flow:
|
|
747
|
+
|
|
748
|
+
```tsx
|
|
749
|
+
"use client";
|
|
750
|
+
import { useCartManager } from "@doswiftly/storefront-sdk/react";
|
|
751
|
+
import { useRouter } from "next/navigation";
|
|
752
|
+
|
|
753
|
+
export function CheckoutSubmit() {
|
|
754
|
+
const router = useRouter();
|
|
755
|
+
const { complete, createPayment, status, error } = useCartManager();
|
|
756
|
+
|
|
757
|
+
async function onSubmit() {
|
|
758
|
+
const { order } = await complete({ idempotencyKey: crypto.randomUUID() });
|
|
759
|
+
// cart-id cookie is already cleared at this point — addItem from here on
|
|
760
|
+
// would create a fresh cart.
|
|
761
|
+
|
|
762
|
+
const successUrl = `/checkout/success?token=${order.accessToken}&orderNumber=${order.orderNumber}`;
|
|
763
|
+
|
|
764
|
+
if (order.canCreatePayment) {
|
|
765
|
+
const session = await createPayment({
|
|
766
|
+
orderId: order.id,
|
|
767
|
+
returnUrl: `${window.location.origin}${successUrl}`,
|
|
768
|
+
});
|
|
769
|
+
if (session.flow === "ONLINE_REDIRECT") {
|
|
770
|
+
window.location.href = session.redirectUrl!;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
router.push(successUrl);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<button onClick={onSubmit} disabled={status.type === "loading"}>
|
|
780
|
+
{status.type === "loading"
|
|
781
|
+
? `Working — ${status.operation}…`
|
|
782
|
+
: "Place order"}
|
|
783
|
+
</button>
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Recovery taxonomy for the new operations**:
|
|
789
|
+
|
|
790
|
+
| Operation | On stale cart | Why |
|
|
791
|
+
| ------------------------- | -------------------------------------------- | ---------------------------------------------------- |
|
|
792
|
+
| `updateAttributes` | Auto-replay via `cartCreate({ attributes })` | Stateless metadata — atomic re-create is safe |
|
|
793
|
+
| `setBillingAddress` | Bail + `cart-expired` event | Separate from shipping; cart must exist |
|
|
794
|
+
| `selectShippingMethod` | Bail + `cart-expired` event | Method is tied to cart contents and shipping address |
|
|
795
|
+
| `selectPaymentMethod` | Bail + `cart-expired` event | Payment selection on existing cart state |
|
|
796
|
+
| `clearPaymentSelection` | Bail + `cart-expired` event | Operates on existing payment fields |
|
|
797
|
+
| `applyGiftCard` | Bail + `cart-expired` event | Balance allocation tied to cart total |
|
|
798
|
+
| `removeGiftCard` | Bail + `cart-expired` event | `giftCardId` refers to a row on the dead cart |
|
|
799
|
+
| `updateGiftCardRecipient` | Bail + `cart-expired` event | `lineId` refers to a line in the dead cart |
|
|
800
|
+
| `complete` | Bail + `cart-expired` event | A finalised cart cannot be auto-recreated |
|
|
801
|
+
| `createPayment` | Out of recovery scope | Operates on `orderId`, not `cartId` |
|
|
802
|
+
|
|
803
|
+
Bail operations fire your existing `onExpired(...)` listener with `event.operation` set — UI shows the same toast/banner you already wired up for `updateItem`/`removeItem`.
|
|
804
|
+
|
|
805
|
+
**Migration checklist** — optional refactor for existing storefronts:
|
|
806
|
+
- [ ] Replace `cartClient.complete({ cartId, ... })` + manual `cookieStore.clear()` with `useCartManager().complete({ ... })`.
|
|
807
|
+
- [ ] Replace `cartClient.selectShippingMethod` / `selectPaymentMethod` / `applyGiftCard` / etc. with the corresponding `useCartManager` methods to get free recovery handling and unified `status` tracking.
|
|
808
|
+
- [ ] If your `onExpired` listener was bound on `useCartManager`, the same listener now also catches stale carts on the new operations — no extra wiring.
|
|
809
|
+
|
|
810
|
+
Calling the raw `CartClient` methods directly still works exactly as before — the new hook surface is purely additive on top.
|
|
811
|
+
|
|
812
|
+
- 468f70b: `useCartManager` now accepts an optional `{ initialCartId }` seed — closes the last gap blocking server-known-cart-id storefronts from adopting the full checkout suite (`complete`, `selectShippingMethod`, `selectPaymentMethod`, …) shipped earlier.
|
|
813
|
+
|
|
814
|
+
**Why** — `useCartManager` reads the cart-id exclusively from the `cart-id` cookie. Storefronts that resolve cart-id server-side (dev env seed for sample storefronts without a product listing, magic-link checkout where the URL carries the cart-id, embedded iframes where the parent supplies cart-id via `postMessage`, customer-service "view this cart" tools, server-side recovery, multi-cart B2B selectors) had no clean way to feed that value into the hook. The workaround was to drop down to raw `CartClient` for every checkout mutation and lose the auto-recovery + `complete` cookie cleanup that the hook provides. This release mirrors the `<StorefrontProvider initialAccessToken>` pattern (v17.1.0) for the cart-id half of the checkout state.
|
|
815
|
+
|
|
816
|
+
**Additive (backward-compatible)**:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
interface UseCartManagerOptions {
|
|
820
|
+
initialCartId?: string | null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function useCartManager(
|
|
824
|
+
options?: UseCartManagerOptions,
|
|
825
|
+
): UseCartManagerResult;
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Calling `useCartManager()` with no arguments is unchanged. Pass `{ initialCartId }` to seed the hook with a server-known value.
|
|
829
|
+
|
|
830
|
+
**Priority order**: `cookie wins → seed → auto-create`. The cookie is the canonical cross-tab state; the seed only fills the gap when the cookie is empty. Once the seed is used, it is eagerly written to the cookie so subsequent reads (other tabs, the next mutation, `getCart`) observe a single source of truth.
|
|
831
|
+
|
|
832
|
+
**Stale seed handling** — no special-case code path. A stale seed flows through the standard recovery pipeline:
|
|
833
|
+
- `addItem` (and other auto-replay ops) → backend returns `CART_NOT_FOUND` → runner clears the cookie, calls `cartCreate({ lines })` against a fresh cart, writes the new id to the cookie, returns the operation result. Storefront sees a successful `addItem` with a populated cart.
|
|
834
|
+
- `updateItem` / `removeItem` / `selectShippingMethod` / `selectPaymentMethod` / `complete` / other state-dependent ops → runner clears the cookie, emits `cart-expired`, throws `CartRecoveryNotPossibleError`. UI shows the toast/banner already wired up for the cookie-driven flow — no extra subscription needed.
|
|
835
|
+
|
|
836
|
+
**Usage example** — magic-link checkout (cart-id resolved server-side from URL params, passed through Server Component to Client Component):
|
|
837
|
+
|
|
838
|
+
```tsx
|
|
839
|
+
// app/checkout/[token]/page.tsx — Server Component
|
|
840
|
+
import { cookies } from "next/headers";
|
|
841
|
+
import { CART_COOKIE_NAME } from "@doswiftly/storefront-sdk";
|
|
842
|
+
import { CheckoutClient } from "./CheckoutClient";
|
|
843
|
+
import { resolveCartIdFromMagicLinkToken } from "@/lib/magic-link";
|
|
844
|
+
|
|
845
|
+
export default async function CheckoutPage({
|
|
846
|
+
params,
|
|
847
|
+
}: {
|
|
848
|
+
params: Promise<{ token: string }>;
|
|
849
|
+
}) {
|
|
850
|
+
const { token } = await params;
|
|
851
|
+
const cookieJar = await cookies();
|
|
852
|
+
const initialCartId =
|
|
853
|
+
cookieJar.get(CART_COOKIE_NAME)?.value ??
|
|
854
|
+
(await resolveCartIdFromMagicLinkToken(token));
|
|
855
|
+
|
|
856
|
+
return <CheckoutClient initialCartId={initialCartId} />;
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
```tsx
|
|
861
|
+
// app/checkout/[token]/CheckoutClient.tsx
|
|
862
|
+
"use client";
|
|
863
|
+
|
|
864
|
+
import { useCartManager } from "@doswiftly/storefront-sdk/react";
|
|
865
|
+
|
|
866
|
+
export function CheckoutClient({
|
|
867
|
+
initialCartId,
|
|
868
|
+
}: {
|
|
869
|
+
initialCartId: string | null;
|
|
870
|
+
}) {
|
|
871
|
+
// Hook handles seed → cookie → recovery transparently. No raw CartClient needed.
|
|
872
|
+
const { complete, selectPaymentMethod, addItem, status } = useCartManager({
|
|
873
|
+
initialCartId,
|
|
874
|
+
});
|
|
875
|
+
// ... checkout flow exactly as for a cookie-driven storefront
|
|
876
|
+
return /* ... */;
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
**Migration checklist** — none required for existing storefronts. The new option is opt-in. To adopt:
|
|
881
|
+
- [ ] Identify your server-known cart-id source (URL params, env var for dev fixtures, parent `postMessage` for iframes, internal admin lookup).
|
|
882
|
+
- [ ] Resolve it in a Server Component (Next.js App Router) or Route Handler, pass through to a Client Component as a prop.
|
|
883
|
+
- [ ] In the Client Component, pass it to `useCartManager({ initialCartId })`.
|
|
884
|
+
- [ ] Remove any previous workaround that manually set the `cart-id` cookie before the hook mounted.
|
|
885
|
+
|
|
886
|
+
Calling `useCartManager` with no options or with `{ initialCartId: null }` continues to behave exactly as before.
|
|
887
|
+
|
|
888
|
+
**Standards reference** — symmetric with the `initialAccessToken` prop on `<StorefrontProvider>` (v17.1.0): both are seeds for first-render state that the client cannot read from the canonical source on its own. Same priority semantics (`canonical store wins → seed → fallback`), same eager-promotion behaviour (seed flows into the canonical store on first use).
|
|
889
|
+
|
|
890
|
+
### Patch Changes
|
|
891
|
+
|
|
892
|
+
- 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.
|
|
893
|
+
- 9857460: Version-sync with `@doswiftly/storefront-operations`: the bundled GraphQL schema now carries clean English descriptions (no code changes).
|
|
894
|
+
- a5a3f4b: Image transform pipeline extended to shop-owned content surfaces: `BlogCategory.image`, `BlogPost.featuredImage`, `LoyaltyReward.image`, and `MenuItem.image`.
|
|
895
|
+
|
|
896
|
+
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.
|
|
897
|
+
|
|
898
|
+
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).
|
|
899
|
+
|
|
900
|
+
**What this enables**:
|
|
901
|
+
|
|
902
|
+
```graphql
|
|
903
|
+
query BlogList {
|
|
904
|
+
blogPosts(first: 10) {
|
|
905
|
+
edges {
|
|
906
|
+
node {
|
|
907
|
+
title
|
|
908
|
+
featuredImage {
|
|
909
|
+
url(transform: { maxWidth: 800, preferredContentType: WEBP })
|
|
910
|
+
altText
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
The same `image { url(transform: ...) }` pattern is also valid on `BlogCategory`, `LoyaltyReward`, and `MenuItem`.
|
|
919
|
+
|
|
920
|
+
**No migration required.**
|
|
921
|
+
|
|
922
|
+
- 0c0205d: Image transform support extended to `Cart.lines[].variant.image`, `Order.lineItems[].variant.image`, `Collection.image`, and `Category.image`.
|
|
923
|
+
|
|
924
|
+
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.
|
|
925
|
+
|
|
926
|
+
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.
|
|
927
|
+
|
|
928
|
+
**What you can do now**:
|
|
929
|
+
|
|
930
|
+
```graphql
|
|
931
|
+
query CartThumbnails {
|
|
932
|
+
cart {
|
|
933
|
+
lines {
|
|
934
|
+
edges {
|
|
935
|
+
node {
|
|
936
|
+
variant {
|
|
937
|
+
image {
|
|
938
|
+
url(transform: { maxWidth: 200, preferredContentType: WEBP })
|
|
939
|
+
altText
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
The same `image { url(transform: ...) }` pattern is now valid on `Order.lineItems[].variant`, `Collection.image`, and `Category.image`.
|
|
950
|
+
|
|
951
|
+
**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.
|
|
952
|
+
|
|
953
|
+
- 9c55b39: Export the payment enums `PaymentProvider`, `PaymentInstrumentType`, `PaymentInstrumentDisplayHint` and `PaymentMethodUnavailableReason` from the package root.
|
|
954
|
+
|
|
955
|
+
**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.
|
|
956
|
+
|
|
957
|
+
**Additive (backward-compatible)**: each enum is both a runtime value (`Object.values(PaymentProvider)`) and a type (`import type { PaymentProvider }`). No existing import changes.
|
|
958
|
+
|
|
959
|
+
**Usage example**:
|
|
960
|
+
|
|
961
|
+
```ts
|
|
962
|
+
import { PaymentProvider } from "@doswiftly/storefront-sdk";
|
|
963
|
+
|
|
964
|
+
// Type a wrapper prop:
|
|
965
|
+
type SelectArgs = { preferredProvider?: PaymentProvider };
|
|
966
|
+
|
|
967
|
+
// Or read the value at runtime:
|
|
968
|
+
const all = Object.values(PaymentProvider); // ['PAYU', 'PRZELEWY24', ...]
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Migration checklist**: optional — if you used a deep import to reach these enums, switch to the package root import.
|
|
972
|
+
|
|
973
|
+
`@doswiftly/storefront-operations` carries a version-sync bump only — no operation or schema change.
|
|
974
|
+
|
|
975
|
+
- 8c0d8fa: Version-sync release — no code changes in this package.
|
|
976
|
+
|
|
977
|
+
Released together with `@doswiftly/storefront-operations` (linked pair) so consumers can pick up the new `./operations.json` subpath export without a mismatched-version warning.
|
|
978
|
+
|
|
3
979
|
## 17.0.0
|
|
4
980
|
|
|
5
981
|
### Major Changes
|