@doswiftly/storefront-operations 15.1.0 → 16.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,10 +27,10 @@ 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**: 15.1.0
31
- - **Queries**: 51
30
+ - **Schema version**: 16.1.0
31
+ - **Queries**: 52
32
32
  - **Mutations**: 40
33
- - **Fragments**: 101
33
+ - **Fragments**: 102
34
34
  <!-- AUTOGEN:STATS:END -->
35
35
 
36
36
  ## Loading order
@@ -174,6 +174,29 @@ semantics). Idempotency is the caller's responsibility. If you need retries
174
174
  on a mutation, wrap with your own backoff logic and check `userErrors` to
175
175
  decide whether to retry.
176
176
 
177
+ ### Configuration sources — `doswiftly.config.ts` first, env vars only as fallback
178
+
179
+ `@doswiftly/storefront-sdk` takes `apiUrl` and `shopSlug` as **explicit
180
+ `config`** on `createStorefrontClient` / `<StorefrontProvider>`. The SDK does
181
+ not read env vars, sniff hostnames, or inspect request headers — the
182
+ storefront supplies the values.
183
+
184
+ Scaffolded storefronts (`doswiftly init`) ship a config helper
185
+ (`lib/graphql/config.ts`) that walks three sources in order:
186
+
187
+ 1. **`doswiftly.config.ts`** — preferred. Committed config file generated at
188
+ scaffolding. No env wiring needed in normal use.
189
+ 2. **`NEXT_PUBLIC_API_URL` + `NEXT_PUBLIC_SHOP_SLUG`** — fallback for local
190
+ dev or storefronts scaffolded outside `doswiftly init`. `doswiftly dev`
191
+ rewrites `NEXT_PUBLIC_API_URL` at runtime to point at a local CORS proxy.
192
+ 3. **`http://localhost:8000` + `demo-shop`** — last-resort defaults.
193
+
194
+ If you generate code that falls back to env vars, use **exactly these names**.
195
+ **Do NOT invent alternatives** like `API_URL`, `STOREFRONT_URL`,
196
+ `TENANT_SLUG` — `doswiftly dev` keys off the canonical names; alternatives
197
+ mean the proxy starts but the storefront still calls the production API
198
+ directly, and CORS errors only surface on the first client-side mutation.
199
+
177
200
  ## When in doubt
178
201
 
179
202
  - **Schema question** ("what fields are on `X`?") → grep `schema.graphql`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,374 @@
1
1
  # Changelog
2
2
 
3
+ ## 16.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1e6062e: Read the gift card recipient back from the cart.
8
+
9
+ `cartUpdateGiftCardRecipient` already persisted the recipient (email, name,
10
+ personalised message) on a gift card line — but the `CartLine` type didn't
11
+ expose any field to read it. Storefronts that render the checkout server-side
12
+ (SSR, deep link, "back to cart" navigation) had no way to know that the buyer
13
+ had already filled in the recipient form on a previous visit. The recipient
14
+ dialog opened blank, like nothing had been saved.
15
+
16
+ **New field** — `CartLine.giftCardRecipient: GiftCardLineRecipient`
17
+
18
+ ```graphql
19
+ type GiftCardLineRecipient {
20
+ recipientEmail: String
21
+ recipientName: String
22
+ message: String
23
+ }
24
+ ```
25
+
26
+ All three fields are individually nullable so a partial set (email only, no
27
+ name or message) is representable as-is. The field on `CartLine` itself is
28
+ nullable — `null` means the buyer has not set a recipient yet, in which case
29
+ the gift card is delivered to the order `customerEmail` on completion.
30
+
31
+ `null` is also what you get for any non-gift-card line — there is no risk of
32
+ the field reporting a stale recipient from a previous line at the same index.
33
+
34
+ **Use it to seed your form**
35
+
36
+ ```tsx
37
+ const cart = await cartClient.get(cartId);
38
+ const giftLine = cart?.lines.edges.find(
39
+ (e) => e.node.productType === "GIFT_CARD",
40
+ );
41
+
42
+ <GiftRecipientForm
43
+ defaultValues={
44
+ giftLine?.node.giftCardRecipient ?? {
45
+ recipientEmail: "",
46
+ recipientName: "",
47
+ message: "",
48
+ }
49
+ }
50
+ onSubmit={(values) =>
51
+ cartClient.updateGiftCardRecipient(cartId, giftLine!.node.id, values)
52
+ }
53
+ />;
54
+ ```
55
+
56
+ **Migration**
57
+
58
+ Additive change — no rename, no signature change. The `CartLine` fragment
59
+ shipped by the SDK already includes the new field after this release, so
60
+ upgrading immediately unlocks the read path. No client code changes required
61
+ to keep existing flows working.
62
+
63
+ - ac2e306: Distinguish recoverable session loss from permanent cart denial.
64
+
65
+ Adds a new `CART_UNAUTHENTICATED` code to the cart error envelope. Until now,
66
+ both "your sign-in expired" and "this cart belongs to someone else" surfaced
67
+ as `CART_FORBIDDEN`, so a storefront had no way to know whether to send the
68
+ buyer to `/login` (recoverable) or to drop the cart cookie (permanent).
69
+ After the typical ~24h session TTL, the buyer would silently lose their
70
+ checkout state instead of being prompted to re-authenticate.
71
+
72
+ **New code** — `CART_UNAUTHENTICATED`
73
+
74
+ | Condition | Code | Recovery |
75
+ | ------------------------------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------- |
76
+ | Anonymous request **or** expired/invalid session on a cart that belongs to a customer | `CART_UNAUTHENTICATED` | Re-authenticate. Cart is preserved (`cart-id` cookie kept). |
77
+ | Authenticated as a **different** customer than the cart owner | `CART_FORBIDDEN` (unchanged) | Drop the cart cookie and start over. |
78
+
79
+ The cart resource itself is intact in the `CART_UNAUTHENTICATED` case — the
80
+ buyer's saved address, line items, discount codes, and gift cards return as
81
+ soon as they sign in.
82
+
83
+ **New SDK signal** — `runner.onSessionExpired(...)`
84
+
85
+ The recovery runner now exposes a separate event for session loss, distinct
86
+ from `onExpired` (which is for cart-resource lifecycle: cart not found, cart
87
+ already completed). Subscribe once at the provider level and route the buyer
88
+ to `/login` with a `return_to` parameter; the cart cookie stays put.
89
+
90
+ ```ts
91
+ // app/providers.tsx
92
+ const unsubExpired = runner.onExpired(() => {
93
+ // cart resource gone — start fresh
94
+ router.push("/cart/new");
95
+ });
96
+
97
+ const unsubSession = runner.onSessionExpired(() => {
98
+ // session gone, cart preserved — sign back in
99
+ router.push(`/login?return_to=${encodeURIComponent(location.pathname)}`);
100
+ });
101
+ ```
102
+
103
+ The session branch throws `CartSessionRequiredError` (re-exported alongside
104
+ `CartRecoveryNotPossibleError`) and does **not** invoke `recreateAndRun` —
105
+ recreating the cart on a stale session would discard the buyer's progress.
106
+
107
+ **Imperative handling**
108
+
109
+ If you handle mutation errors directly (Server Action, custom hook), add one
110
+ case to your existing switch:
111
+
112
+ ```ts
113
+ switch (err.userErrors[0]?.code) {
114
+ case "CART_UNAUTHENTICATED":
115
+ redirect(`/login?return_to=${encodeURIComponent("/checkout")}`);
116
+ break;
117
+ case "CART_FORBIDDEN":
118
+ cookies().delete("cart-id");
119
+ redirect("/cart/expired");
120
+ break;
121
+ // ...existing cases for CART_NOT_FOUND / ALREADY_COMPLETED
122
+ }
123
+ ```
124
+
125
+ **Migration**
126
+
127
+ Additive change — no rename, no signature change. Existing `case 'CART_FORBIDDEN'`
128
+ branches keep working as before. Opt in to the new code at your own pace; until
129
+ you add the case, anonymous and cross-customer denials fall into your existing
130
+ default branch (one will be recoverable, one will not — adding the case lets
131
+ you distinguish them).
132
+
133
+ ## 16.0.0
134
+
135
+ ### Major Changes
136
+
137
+ - ca67b92: **BREAKING** — the formatting utilities switch from a hardcoded 13-currency map to runtime-driven `Intl.NumberFormat` resolution. Every ISO 4217 currency known to the runtime is now supported, with the locale-correct symbol and separators.
138
+
139
+ **API changes**
140
+ - `formatDate(date, locale)` — `locale` is now a required argument. The previous hardcoded `'en-US'` default is gone.
141
+ - `formatDateTime(date, locale)` — `locale` required.
142
+ - `formatNumber(num, locale)` — `locale` required.
143
+ - `formatPrice(price, locale?)` — `locale` is now optional. When omitted, the runtime default (`Intl.NumberFormat().resolvedOptions().locale`) is used. The previous `CURRENCY_LOCALES`-keyed auto-selection is gone.
144
+ - `formatAmount(amount, currencyCode, locale?)` — third argument added (optional).
145
+ - `formatPriceRange(min, max, locale?)` — third argument added (optional).
146
+ - `getCurrencySymbol(code, locale?)` — second argument added (optional). The symbol is derived from `Intl.NumberFormat.formatToParts()`; output depends on the locale (`getCurrencySymbol('PLN', 'pl-PL') === 'zł'`, `getCurrencySymbol('PLN', 'en-US') === 'PLN'`).
147
+
148
+ **Removed exports**
149
+ - `CURRENCY_SYMBOLS` — the 13-entry map is gone. Use `getCurrencySymbol(code, locale)` instead, or read the symbol from `Intl.NumberFormat.formatToParts()` directly.
150
+ - `CURRENCY_LOCALES` — the locale-to-currency map is gone. Pick the locale from the storefront's i18n context (or read `shop.localeToCurrencyMap` from the API for per-shop overrides) and pass it to `formatPrice` / `formatAmount`.
151
+
152
+ **Why**
153
+ - The hardcoded 13-currency map covered ~7% of the ISO 4217 currencies the API now ships in `CurrencyCode` (~180 entries). Every currency outside that list silently fell back to `en-US` formatting — a Brazilian (BRL) or Indian (INR) storefront had to either write its own formatter or accept US-style output.
154
+ - Locale↔currency is a **per-shop** decision (`shop.localeToCurrencyMap` in the schema is explicitly marked SSOT). A globally-hardcoded map in the SDK is the wrong place for that knowledge — it makes Polish customers of a multi-locale shop unable to see PLN in `'en-PL'` even when the merchant configured it.
155
+ - Money precision: `formatPrice` and `formatAmount` no longer apply `parseFloat` to `Money.amount`. Decimal strings are forwarded to `Intl.NumberFormat.format()` verbatim, preserving precision across locales and unusual subunit currencies (JPY/KRW zero-decimal, BHD/JOD/KWD three-decimal, ISK rounding).
156
+
157
+ **Migration**
158
+
159
+ ```ts
160
+ // Before
161
+ import {
162
+ formatDate,
163
+ formatPrice,
164
+ CURRENCY_LOCALES,
165
+ CURRENCY_SYMBOLS,
166
+ } from "@doswiftly/storefront-sdk";
167
+
168
+ formatDate(order.createdAt); // hardcoded "Dec 9, 2025"
169
+ formatPrice({ amount: "12.50", currencyCode: "PLN" }); // auto-picked pl-PL → "12,50 zł"
170
+ CURRENCY_LOCALES.PLN; // "pl-PL"
171
+ CURRENCY_SYMBOLS.PLN; // "zł"
172
+
173
+ // After (Client Components)
174
+ import { useLocale } from "next-intl";
175
+ import {
176
+ formatDate,
177
+ formatPrice,
178
+ getCurrencySymbol,
179
+ } from "@doswiftly/storefront-sdk";
180
+
181
+ const locale = useLocale(); // e.g. "pl-PL"
182
+ formatDate(order.createdAt, locale);
183
+ formatPrice({ amount: "12.50", currencyCode: "PLN" }, locale);
184
+ getCurrencySymbol("PLN", locale); // "zł"
185
+
186
+ // After (Server Components / Route Handlers)
187
+ import { getLocale } from "next-intl/server";
188
+ const locale = await getLocale();
189
+ formatDate(order.createdAt, locale);
190
+ ```
191
+
192
+ For per-shop locale↔currency overrides, read `shop.localeToCurrencyMap` from the Storefront API and resolve the locale yourself before calling `formatPrice` / `formatAmount`.
193
+
194
+ **New: Context-driven format hooks**
195
+
196
+ `@doswiftly/storefront-sdk/react` now ships convenience hooks that pull the active language from `useLanguageStore` (inside `<StorefrontProvider>`) and return a memoised, locale-bound formatter. Use them when you don't want to pass `locale` to every call:
197
+
198
+ ```ts
199
+ import {
200
+ useFormatPrice,
201
+ useFormatAmount,
202
+ useFormatPriceRange,
203
+ useFormatDate,
204
+ useFormatDateTime,
205
+ useFormatNumber,
206
+ useGetCurrencySymbol,
207
+ } from '@doswiftly/storefront-sdk/react';
208
+
209
+ function Cart() {
210
+ const formatPrice = useFormatPrice();
211
+ return <span>{formatPrice(item.price)}</span>;
212
+ }
213
+
214
+ function OrderRow() {
215
+ const formatDate = useFormatDate();
216
+ return <span>{formatDate(order.processedAt)}</span>;
217
+ }
218
+ ```
219
+
220
+ Each hook accepts an optional last `localeOverride` argument that wins over the store value — useful for a "show in US format" toggle on a single element:
221
+
222
+ ```ts
223
+ const formatPrice = useFormatPrice();
224
+ return <span>{formatPrice(item.price, 'en-US')}</span>;
225
+ ```
226
+
227
+ Resolution per call: `localeOverride` → `useLanguageStore().language` → runtime default.
228
+
229
+ Use the vanilla `formatPrice` / `formatDate` / etc. from `@doswiftly/storefront-sdk` (no hook) with an explicit `locale` for server components, e-mail templates, or any code path outside a provider.
230
+
231
+ `formatPercentage` is unchanged.
232
+
233
+ `@doswiftly/storefront-operations` bumped to keep linked parity — no operations changes.
234
+
235
+ ### Minor Changes
236
+
237
+ - 885c011: Re-export checkout types from the public API. `CartClient` methods added in 15.1.0 (`getAvailablePaymentMethods`, `getAvailableShippingMethods`) returned typed payloads, but the named types were not part of the public surface — component props and function signatures touching these payloads required `Awaited<ReturnType<...>>` workarounds. This change closes that gap.
238
+
239
+ **Newly exported types:**
240
+ - `PaymentMethod`, `AvailablePaymentMethods` — shape returned by `CartClient.getAvailablePaymentMethods()`
241
+ - `AvailableShippingMethod`, `AvailableShippingMethodsPayload`, `FreeShippingProgress`, `DeliveryEstimate`, `ShippingCarrier`, `DeliveryType` — shape returned by `CartClient.getAvailableShippingMethods()`
242
+ - `PickupPoint`, `PickupPointInput` — locker / pickup-point delivery (surfaced via `MailingAddress.pickupPoint` and `CartAddressInput.pickupPoint`)
243
+ - `CartAttributeInput` — input shape for `CartClient.updateAttributes()`
244
+ - `ShippingAddressInput` — input shape for the `availableShippingMethods` standalone query
245
+
246
+ **Example:**
247
+
248
+ ```ts
249
+ import type {
250
+ AvailableShippingMethodsPayload,
251
+ AvailableShippingMethod,
252
+ DeliveryType,
253
+ } from "@doswiftly/storefront-sdk";
254
+
255
+ function isPickup(method: AvailableShippingMethod): boolean {
256
+ return method.deliveryType === "PICKUP_POINT";
257
+ }
258
+ ```
259
+
260
+ No runtime changes — types already existed internally; this change only adds them to the published surface.
261
+
262
+ `@doswiftly/storefront-operations` bumped to keep linked parity — no operations changes.
263
+
264
+ - fd3d199: Expose `cart.cost.totalDiscount` and `cart.cost.totalShipping` through the `CartCost` fragment.
265
+
266
+ The schema already exposed `CartCost.totalDiscount: Money!` (aggregate of every entry in `cart.discountAllocations`) and `CartCost.totalShipping: Money` (cost of the currently selected shipping method, `null` until one is selected), but the shared `CartCost` fragment did not select them — so they were not part of the typed `Cart.cost` returned by `CartClient`. Storefronts wanting to render a single "Discounts: -X" row or a shipping summary had to either sum `discountAllocations` client-side (precision-sensitive across currencies) or query the schema directly and bypass the typed surface.
267
+
268
+ After this release, both fields are part of `Cart.cost` returned by every `CartClient` operation — `get`, `create`, `addItems`, `updateItems`, `removeItems`, `setShippingAddress`, `setBillingAddress`, `selectShippingMethod`, `selectPaymentMethod`, `updateBuyerIdentity`, `updateDiscountCodes`, `updateNote`, `updateAttributes`, `applyGiftCard`, `removeGiftCard`, `updateGiftCardRecipient`.
269
+
270
+ **Example:**
271
+
272
+ ```ts
273
+ const cart = await cartClient.get(cartId);
274
+
275
+ const summary = {
276
+ subtotal: cart.cost.subtotal,
277
+ discount: cart.cost.totalDiscount, // Money — defaults to amount 0
278
+ shipping: cart.cost.totalShipping, // Money | null — null until selectShippingMethod()
279
+ tax: cart.cost.totalTax,
280
+ total: cart.cost.total, // grand total — use directly on checkout summary
281
+ };
282
+ ```
283
+
284
+ `totalDiscount` is required (defaults to amount 0 when no discount applies). `totalShipping` is nullable — `null` means no shipping method has been selected yet, an amount of 0 means a free-shipping method was selected.
285
+
286
+ Additive — existing consumers keep every field they had before, no breaking change.
287
+
288
+ `@doswiftly/storefront-operations` bumped to keep linked parity (fragment update).
289
+
290
+ - 51091df: Expose `Cart.status` and `Cart.completedOrder` so storefronts can detect a completed (or expired / abandoned) cart on read, without having to attempt a mutation first and react to its `userErrors[].code`.
291
+
292
+ **New fields**
293
+ - `Cart.status: CartStatus!` — lifecycle status (`ACTIVE`, `ABANDONED`, `CONVERTED`, `RECOVERED`, `EXPIRED`). Only `ACTIVE` carts accept mutations; any other status rejects subsequent mutations with `CartErrorCode.ALREADY_COMPLETED`.
294
+ - `Cart.completedOrder: Order` — the order this cart converted into. Populated only when `status === CONVERTED`; null on every other status.
295
+
296
+ **Why**
297
+
298
+ When a buyer returned to the checkout page after completing their order (SSR re-render, deep link, "back" button after redirect), the SDK could only ask `cart(id)` for the cart and got the full pre-completion state back — every form value still there, the "Pay" button still active. The first mutation then failed with `ALREADY_COMPLETED`, after the buyer had already filled out fields. With `status` exposed, the storefront detects the terminal state on initial render and redirects to the order confirmation directly — using the `accessToken` on `completedOrder` for guest tracking, no extra `orderByToken` round-trip.
299
+
300
+ **Example**
301
+
302
+ ```ts
303
+ const cart = await cartClient.get(cartId);
304
+ if (!cart) {
305
+ // Cart not found — guide the buyer to create a new one
306
+ return redirect("/cart");
307
+ }
308
+ if (cart.status !== "ACTIVE") {
309
+ if (cart.completedOrder) {
310
+ // Render the order confirmation page off the data you already have
311
+ return redirect(`/order/${cart.completedOrder.accessToken}`);
312
+ }
313
+ // Abandoned / expired — start a fresh cart
314
+ return redirect("/cart/new");
315
+ }
316
+ // Render the checkout form normally
317
+ ```
318
+
319
+ Additive — every existing query that selects the shared `Cart` fragment now sees the two new fields automatically. No breaking change.
320
+
321
+ `@doswiftly/storefront-operations` bumped to keep linked parity — schema sync delivers the new fields and the `CartStatus` enum.
322
+
323
+ - f4efab9: Add `ShopConfigFields` fragment + `query ShopConfig` for `<StorefrontProvider>` setup.
324
+
325
+ `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react` expects a `ShopConfig` payload with a specific shape: currency setup (including `localeToCurrencyMap`), language setup, and bot protection. Until now the storefront had to hand-write the field selection, and it was easy to miss `localeToCurrencyMap` (used internally by the SDK for browser-locale-based currency detection) or to add an extra field that didn't match the `ShopConfig` interface.
326
+
327
+ **New**
328
+ - `fragment ShopConfigFields on Shop` — minimal selection that matches the `ShopConfig` interface 1:1.
329
+ - `query ShopConfig { shop { ...ShopConfigFields } }` — ready-to-use query for the provider.
330
+
331
+ **Example**
332
+
333
+ ```ts
334
+ const SHOP_CONFIG_QUERY = /* GraphQL */ `
335
+ query ShopConfig {
336
+ shop {
337
+ ...ShopConfigFields
338
+ }
339
+ }
340
+ `;
341
+
342
+ // In a Server Component:
343
+ const { data } = await execute(SHOP_CONFIG_QUERY);
344
+ return <StorefrontProvider shopData={data.shop}>{children}</StorefrontProvider>;
345
+ ```
346
+
347
+ Use the larger `Shop` fragment + `query Shop` when you also need branding / contact / business hours for your UI; use `ShopConfig` when you only need to bootstrap the provider.
348
+
349
+ Additive — no breaking change.
350
+
351
+ `@doswiftly/storefront-sdk` bumped to keep linked parity — no code change.
352
+
353
+ ### Patch Changes
354
+
355
+ - 7ce8ac4: Clarify how `apiUrl` and `shopSlug` are configured.
356
+
357
+ `createStorefrontClient` and `<StorefrontProvider>` take `apiUrl` and `shopSlug` as **explicit `config`** — the SDK does not read environment variables, sniff hostnames, or inspect request headers. The storefront supplies the values; the SDK uses them verbatim.
358
+
359
+ Scaffolded storefronts ship a config helper (`lib/graphql/config.ts`) that resolves the two values from these sources, in order:
360
+ 1. `doswiftly.config.ts` (preferred — committed file generated at `doswiftly init`)
361
+ 2. `NEXT_PUBLIC_API_URL` + `NEXT_PUBLIC_SHOP_SLUG` (fallback, used for local development)
362
+ 3. `http://localhost:8000` + `demo-shop` (defaults — smoke test only)
363
+
364
+ Scratch-built storefronts can skip the helper and pass values into `config={}` directly.
365
+
366
+ **What's new**
367
+ - `@doswiftly/storefront-sdk` README: new `## Configuration` section between `## Installation` and `## Quick Start`. Documents the explicit-config requirement, the three-source resolver shipped with scaffolded storefronts, and when env-var wiring actually matters.
368
+ - `@doswiftly/storefront-operations` `AGENTS.md`: new `### Configuration sources` convention inside `## Critical conventions — DO NOT hallucinate`. AI assistants now prefer `doswiftly.config.ts` and only fall back to the canonical env-var names.
369
+
370
+ Documentation only — no code change.
371
+
3
372
  ## 15.1.0
4
373
 
5
374
  ### Minor Changes
package/README.md CHANGED
@@ -163,6 +163,7 @@ full executable body of each operation.
163
163
  | Operation | Description |
164
164
  | --- | --- |
165
165
  | `Shop` | Returns shop configuration: name, base + supported currencies, supported locales, branding (logo, colors, fonts, social links), contact info, active payment methods, brand metadata, money format template, and the list of countries the shop ships to. Public; no auth required. Call once per session and cache — almost everything else is contextualized by the shop returned here. |
166
+ | `ShopConfig` | Minimal Shop payload for `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Returns exactly the fields the SDK's `ShopConfig` interface declares — currency setup (with `localeToCurrencyMap` for browser-locale-based currency detection), language setup, and bot protection. Cache for the session; refetch when the merchant updates currency / language settings or you want to pick up new bot-protection rules. |
166
167
 
167
168
  #### Products
168
169
 
@@ -476,7 +477,7 @@ full executable body of each operation.
476
477
 
477
478
  | Fragment | On Type | Description |
478
479
  | --- | --- | --- |
479
- | `CartCost` | `CartCost` | Cart-level totals — subtotal, total, tax, duty, checkout charge. Spread inside the `Cart` fragment. |
480
+ | `CartCost` | `CartCost` | Cart-level totals — subtotal, total, tax, duty, discount, shipping, checkout charge. Spread inside the `Cart` fragment. |
480
481
  | `CartLineCost` | `CartLineCost` | Per-line cost breakdown — unit price, line subtotal/total, compare-at unit price (for showing strikethroughs). Spread inside `CartLine`. |
481
482
  | `AttributeSelection` | `AttributeSelection` | Typed snapshot of a customer-filled product attribute (configurator selection) on a cart line. Includes the chosen option / text value, fillingMode, billingMode, surcharge, tax class, and any linked variant. Distinct from `CartLine.attributes` which holds raw key-value line item properties (free-form, untyped). |
482
483
  | `CartLine` | `CartLine` | A single line in the cart — variant + quantity + per-line cost, plus dual attribute storage: `attributes[]` for free-form key-value properties (gift wrap, engraving notes), `attributeSelections[]` for typed configurator answers. |
@@ -501,6 +502,7 @@ full executable body of each operation.
501
502
  | `BotProtectionProvider` | `BotProtectionProviderInfo` | Bot-protection provider config (provider name, site key, script URL) for storefront-side challenge widgets. |
502
503
  | `BotProtection` | `BotProtectionInfo` | Bot-protection setup for the shop — primary + fallback provider, and the list of `protectedOperations` (mutation names that require a challenge token). Spread inside `Shop`. |
503
504
  | `Shop` | `Shop` | The shop itself — name, primary domain, currencies + locales, branding, contact info, business hours, bot-protection config. Spread on the root `shop` query; cache for the session. |
505
+ | `ShopConfigFields` | `Shop` | Minimal Shop fields consumed by `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Spread this in your `shop` query to get a response shape that matches the SDK `ShopConfig` interface 1:1 — no manual field selection, no drift when the SDK adds optional fields. Use the larger `Shop` fragment when you also need branding / contact / business hours for your UI. |
504
506
 
505
507
  #### Payment Methods
506
508
 
package/fragments.graphql CHANGED
@@ -347,7 +347,7 @@ fragment Order on Order {
347
347
  # Cart
348
348
  # ============================================
349
349
 
350
- # Cart-level totals — subtotal, total, tax, duty, checkout charge. Spread inside the `Cart` fragment.
350
+ # Cart-level totals — subtotal, total, tax, duty, discount, shipping, checkout charge. Spread inside the `Cart` fragment.
351
351
  fragment CartCost on CartCost {
352
352
  total {
353
353
  ...Money
@@ -364,6 +364,12 @@ fragment CartCost on CartCost {
364
364
  checkoutCharge {
365
365
  ...Money
366
366
  }
367
+ totalDiscount {
368
+ ...Money
369
+ }
370
+ totalShipping {
371
+ ...Money
372
+ }
367
373
  }
368
374
 
369
375
  # Per-line cost breakdown — unit price, line subtotal/total, compare-at unit price (for showing strikethroughs). Spread inside `CartLine`.
@@ -421,6 +427,11 @@ fragment CartLine on CartLine {
421
427
  productHandle
422
428
  productType
423
429
  requiresShipping
430
+ giftCardRecipient {
431
+ recipientEmail
432
+ recipientName
433
+ message
434
+ }
424
435
  }
425
436
 
426
437
  # Buyer identity associated with the cart. Note: only `customerId` is currently persisted server-side; `email`, `phone`, `countryCode` are accepted in the input but ignored.
@@ -505,6 +516,15 @@ fragment Cart on Cart {
505
516
  requiresShipping
506
517
  createdAt
507
518
  updatedAt
519
+ status
520
+ completedOrder {
521
+ id
522
+ orderNumber
523
+ accessToken
524
+ status
525
+ paymentStatus
526
+ fulfillmentStatus
527
+ }
508
528
  }
509
529
 
510
530
  # Shipping method selected on a cart (D8 term unification — wcześniej ShippingRate). Returned przez `cart.selectedShippingMethod` po `cartSelectShippingMethod` mutation.
@@ -661,6 +681,21 @@ fragment Shop on Shop {
661
681
  }
662
682
  }
663
683
 
684
+ # Minimal Shop fields consumed by `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Spread this in your `shop` query to get a response shape that matches the SDK `ShopConfig` interface 1:1 — no manual field selection, no drift when the SDK adds optional fields. Use the larger `Shop` fragment when you also need branding / contact / business hours for your UI.
685
+ fragment ShopConfigFields on Shop {
686
+ currencyCode
687
+ supportedCurrencies
688
+ localeToCurrencyMap {
689
+ locale
690
+ currency
691
+ }
692
+ defaultLanguage
693
+ supportedLanguages
694
+ botProtection {
695
+ ...BotProtection
696
+ }
697
+ }
698
+
664
699
  # ============================================
665
700
  # Payment Methods
666
701
  # ============================================
package/llms-full.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # DoSwiftly Storefront Operations — Full Reference
2
2
 
3
- > Schema version: **15.1.0**
4
- > 51 queries · 40 mutations · 101 fragments
3
+ > Schema version: **16.1.0**
4
+ > 52 queries · 40 mutations · 102 fragments
5
5
 
6
6
  Auto-generated from `.graphql` source files. Do not edit by hand — this file is
7
7
  regenerated on every release to match the published schema.
@@ -37,6 +37,25 @@ query Shop {
37
37
  }
38
38
  ```
39
39
 
40
+ ### Query: `ShopConfig`
41
+
42
+ **Section**: Shop
43
+
44
+ **Description**: Minimal Shop payload for `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Returns exactly the fields the SDK's `ShopConfig` interface declares — currency setup (with `localeToCurrencyMap` for browser-locale-based currency detection), language setup, and bot protection. Cache for the session; refetch when the merchant updates currency / language settings or you want to pick up new bot-protection rules.
45
+
46
+ **Variables**: none
47
+
48
+ **Fragments used**: `ShopConfigFields`
49
+
50
+ **GraphQL**:
51
+ ```graphql
52
+ query ShopConfig {
53
+ shop {
54
+ ...ShopConfigFields
55
+ }
56
+ }
57
+ ```
58
+
40
59
  ### Query: `Product`
41
60
 
42
61
  **Section**: Products
@@ -2920,7 +2939,7 @@ fragment Order on Order {
2920
2939
 
2921
2940
  **Section**: Cart
2922
2941
 
2923
- **Description**: Cart-level totals — subtotal, total, tax, duty, checkout charge. Spread inside the `Cart` fragment.
2942
+ **Description**: Cart-level totals — subtotal, total, tax, duty, discount, shipping, checkout charge. Spread inside the `Cart` fragment.
2924
2943
 
2925
2944
  **Uses fragments**: `Money`
2926
2945
 
@@ -2942,6 +2961,12 @@ fragment CartCost on CartCost {
2942
2961
  checkoutCharge {
2943
2962
  ...Money
2944
2963
  }
2964
+ totalDiscount {
2965
+ ...Money
2966
+ }
2967
+ totalShipping {
2968
+ ...Money
2969
+ }
2945
2970
  }
2946
2971
  ```
2947
2972
 
@@ -3027,6 +3052,11 @@ fragment CartLine on CartLine {
3027
3052
  productHandle
3028
3053
  productType
3029
3054
  requiresShipping
3055
+ giftCardRecipient {
3056
+ recipientEmail
3057
+ recipientName
3058
+ message
3059
+ }
3030
3060
  }
3031
3061
  ```
3032
3062
 
@@ -3147,6 +3177,15 @@ fragment Cart on Cart {
3147
3177
  requiresShipping
3148
3178
  createdAt
3149
3179
  updatedAt
3180
+ status
3181
+ completedOrder {
3182
+ id
3183
+ orderNumber
3184
+ accessToken
3185
+ status
3186
+ paymentStatus
3187
+ fulfillmentStatus
3188
+ }
3150
3189
  }
3151
3190
  ```
3152
3191
 
@@ -3406,6 +3445,31 @@ fragment Shop on Shop {
3406
3445
  }
3407
3446
  ```
3408
3447
 
3448
+ ### Fragment: `ShopConfigFields` on `Shop`
3449
+
3450
+ **Section**: Shop
3451
+
3452
+ **Description**: Minimal Shop fields consumed by `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Spread this in your `shop` query to get a response shape that matches the SDK `ShopConfig` interface 1:1 — no manual field selection, no drift when the SDK adds optional fields. Use the larger `Shop` fragment when you also need branding / contact / business hours for your UI.
3453
+
3454
+ **Uses fragments**: `BotProtection`
3455
+
3456
+ **GraphQL**:
3457
+ ```graphql
3458
+ fragment ShopConfigFields on Shop {
3459
+ currencyCode
3460
+ supportedCurrencies
3461
+ localeToCurrencyMap {
3462
+ locale
3463
+ currency
3464
+ }
3465
+ defaultLanguage
3466
+ supportedLanguages
3467
+ botProtection {
3468
+ ...BotProtection
3469
+ }
3470
+ }
3471
+ ```
3472
+
3409
3473
  ### Fragment: `PaymentMethod` on `PaymentMethod`
3410
3474
 
3411
3475
  **Section**: Payment Methods
package/operations.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": "15.1.0",
2
+ "schemaVersion": "16.1.0",
3
3
  "queries": [
4
4
  {
5
5
  "name": "Shop",
@@ -12,6 +12,17 @@
12
12
  ],
13
13
  "body": "query Shop {\n shop {\n ...Shop\n }\n}"
14
14
  },
15
+ {
16
+ "name": "ShopConfig",
17
+ "kind": "query",
18
+ "section": "Shop",
19
+ "description": "Minimal Shop payload for `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Returns exactly the fields the SDK's `ShopConfig` interface declares — currency setup (with `localeToCurrencyMap` for browser-locale-based currency detection), language setup, and bot protection. Cache for the session; refetch when the merchant updates currency / language settings or you want to pick up new bot-protection rules.",
20
+ "variables": [],
21
+ "fragmentRefs": [
22
+ "ShopConfigFields"
23
+ ],
24
+ "body": "query ShopConfig {\n shop {\n ...ShopConfigFields\n }\n}"
25
+ },
15
26
  {
16
27
  "name": "Product",
17
28
  "kind": "query",
@@ -2072,12 +2083,12 @@
2072
2083
  "name": "CartCost",
2073
2084
  "kind": "fragment",
2074
2085
  "section": "Cart",
2075
- "description": "Cart-level totals — subtotal, total, tax, duty, checkout charge. Spread inside the `Cart` fragment.",
2086
+ "description": "Cart-level totals — subtotal, total, tax, duty, discount, shipping, checkout charge. Spread inside the `Cart` fragment.",
2076
2087
  "variables": [],
2077
2088
  "fragmentRefs": [
2078
2089
  "Money"
2079
2090
  ],
2080
- "body": "fragment CartCost on CartCost {\n total {\n ...Money\n }\n subtotal {\n ...Money\n }\n totalTax {\n ...Money\n }\n totalDuty {\n ...Money\n }\n checkoutCharge {\n ...Money\n }\n}",
2091
+ "body": "fragment CartCost on CartCost {\n total {\n ...Money\n }\n subtotal {\n ...Money\n }\n totalTax {\n ...Money\n }\n totalDuty {\n ...Money\n }\n checkoutCharge {\n ...Money\n }\n totalDiscount {\n ...Money\n }\n totalShipping {\n ...Money\n }\n}",
2081
2092
  "onType": "CartCost"
2082
2093
  },
2083
2094
  {
@@ -2113,7 +2124,7 @@
2113
2124
  "CartLineCost",
2114
2125
  "ProductVariant"
2115
2126
  ],
2116
- "body": "fragment CartLine on CartLine {\n id\n quantity\n variant {\n ...ProductVariant\n }\n cost {\n ...CartLineCost\n }\n attributes {\n key\n value\n }\n attributeSelections {\n ...AttributeSelection\n }\n productId\n productTitle\n productHandle\n productType\n requiresShipping\n}",
2127
+ "body": "fragment CartLine on CartLine {\n id\n quantity\n variant {\n ...ProductVariant\n }\n cost {\n ...CartLineCost\n }\n attributes {\n key\n value\n }\n attributeSelections {\n ...AttributeSelection\n }\n productId\n productTitle\n productHandle\n productType\n requiresShipping\n giftCardRecipient {\n recipientEmail\n recipientName\n message\n }\n}",
2117
2128
  "onType": "CartLine"
2118
2129
  },
2119
2130
  {
@@ -2166,7 +2177,7 @@
2166
2177
  "MailingAddress",
2167
2178
  "PageInfo"
2168
2179
  ],
2169
- "body": "fragment Cart on Cart {\n id\n checkoutUrl\n totalQuantity\n cost {\n ...CartCost\n }\n lines(first: 100) {\n edges {\n cursor\n node {\n ... on CartLine {\n ...CartLine\n }\n }\n }\n nodes {\n ... on CartLine {\n ...CartLine\n }\n }\n pageInfo {\n ...PageInfo\n }\n totalCount\n }\n buyerIdentity {\n ...CartBuyerIdentity\n }\n discountCodes {\n ...CartDiscountCode\n }\n discountAllocations {\n ...CartDiscountAllocation\n }\n note\n attributes {\n key\n value\n }\n email\n phone\n shippingAddress {\n ...MailingAddress\n }\n billingAddress {\n ...MailingAddress\n }\n selectedShippingMethod {\n ...CartShippingMethod\n }\n selectedPaymentMethod {\n ...CartSelectedPaymentMethod\n }\n appliedGiftCards {\n ...CartAppliedGiftCard\n }\n requiresShipping\n createdAt\n updatedAt\n}",
2180
+ "body": "fragment Cart on Cart {\n id\n checkoutUrl\n totalQuantity\n cost {\n ...CartCost\n }\n lines(first: 100) {\n edges {\n cursor\n node {\n ... on CartLine {\n ...CartLine\n }\n }\n }\n nodes {\n ... on CartLine {\n ...CartLine\n }\n }\n pageInfo {\n ...PageInfo\n }\n totalCount\n }\n buyerIdentity {\n ...CartBuyerIdentity\n }\n discountCodes {\n ...CartDiscountCode\n }\n discountAllocations {\n ...CartDiscountAllocation\n }\n note\n attributes {\n key\n value\n }\n email\n phone\n shippingAddress {\n ...MailingAddress\n }\n billingAddress {\n ...MailingAddress\n }\n selectedShippingMethod {\n ...CartShippingMethod\n }\n selectedPaymentMethod {\n ...CartSelectedPaymentMethod\n }\n appliedGiftCards {\n ...CartAppliedGiftCard\n }\n requiresShipping\n createdAt\n updatedAt\n status\n completedOrder {\n id\n orderNumber\n accessToken\n status\n paymentStatus\n fulfillmentStatus\n }\n}",
2170
2181
  "onType": "Cart"
2171
2182
  },
2172
2183
  {
@@ -2306,6 +2317,18 @@
2306
2317
  "body": "fragment Shop on Shop {\n id\n name\n description\n primaryDomain {\n host\n url\n isSslEnabled\n }\n currencyCode\n supportedCurrencies\n paymentCurrencies\n defaultLanguage\n supportedLanguages\n logo {\n ...Image\n }\n contactEmail\n contactPhone\n address {\n ...ShopAddress\n }\n businessHours {\n ...BusinessHour\n }\n branding {\n ...ShopBranding\n }\n botProtection {\n ...BotProtection\n }\n}",
2307
2318
  "onType": "Shop"
2308
2319
  },
2320
+ {
2321
+ "name": "ShopConfigFields",
2322
+ "kind": "fragment",
2323
+ "section": "Shop",
2324
+ "description": "Minimal Shop fields consumed by `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Spread this in your `shop` query to get a response shape that matches the SDK `ShopConfig` interface 1:1 — no manual field selection, no drift when the SDK adds optional fields. Use the larger `Shop` fragment when you also need branding / contact / business hours for your UI.",
2325
+ "variables": [],
2326
+ "fragmentRefs": [
2327
+ "BotProtection"
2328
+ ],
2329
+ "body": "fragment ShopConfigFields on Shop {\n currencyCode\n supportedCurrencies\n localeToCurrencyMap {\n locale\n currency\n }\n defaultLanguage\n supportedLanguages\n botProtection {\n ...BotProtection\n }\n}",
2330
+ "onType": "Shop"
2331
+ },
2309
2332
  {
2310
2333
  "name": "PaymentMethod",
2311
2334
  "kind": "fragment",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "15.1.0",
3
+ "version": "16.1.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
package/queries.graphql CHANGED
@@ -14,6 +14,13 @@ query Shop {
14
14
  }
15
15
  }
16
16
 
17
+ # Minimal Shop payload for `<StorefrontProvider shopData={...}>` from `@doswiftly/storefront-sdk/react`. Returns exactly the fields the SDK's `ShopConfig` interface declares — currency setup (with `localeToCurrencyMap` for browser-locale-based currency detection), language setup, and bot protection. Cache for the session; refetch when the merchant updates currency / language settings or you want to pick up new bot-protection rules.
18
+ query ShopConfig {
19
+ shop {
20
+ ...ShopConfigFields
21
+ }
22
+ }
23
+
17
24
  # ============================================
18
25
  # Products
19
26
  # ============================================
package/schema.graphql CHANGED
@@ -903,6 +903,11 @@ type Cart implements Node {
903
903
  """
904
904
  checkoutUrl: URL
905
905
 
906
+ """
907
+ The order that this cart converted into. Populated only when `status` is `CONVERTED` — null on every other status. Use this to render the order confirmation page (subtotals, accessToken for guest tracking) directly off the cart you already loaded, without a second `orderByToken` round-trip.
908
+ """
909
+ completedOrder: Order
910
+
906
911
  """
907
912
  Cost breakdown for the cart (subtotal, tax, shipping, discount, grand total).
908
913
  """
@@ -965,6 +970,11 @@ type Cart implements Node {
965
970
  """
966
971
  shippingAddress: MailingAddress
967
972
 
973
+ """
974
+ Lifecycle status — `ACTIVE` for the editable working cart, terminal otherwise. Check this on SSR before rendering the checkout form: a non-`ACTIVE` cart should redirect (typically to the order confirmation when `completedOrder` is populated) instead of presenting a form whose first mutation fails with `CartErrorCode.ALREADY_COMPLETED`.
975
+ """
976
+ status: CartStatus!
977
+
968
978
  """
969
979
  Sum of `quantity` across all lines — the badge number for the cart icon.
970
980
  """
@@ -1350,6 +1360,11 @@ type CartLine {
1350
1360
  """
1351
1361
  cost: CartLineCost!
1352
1362
 
1363
+ """
1364
+ Recipient details captured for a `GIFT_CARD` line via `cartUpdateGiftCardRecipient`. Null for non-gift-card lines or when no recipient has been set yet — in that case the gift card is delivered to the order `customerEmail` on completion. Use to initialise the "gift recipient" form from cart state on SSR / page reload / deep link instead of keeping it in client-only state.
1365
+ """
1366
+ giftCardRecipient: GiftCardLineRecipient
1367
+
1353
1368
  """
1354
1369
  Stable line identifier. Use it to address the line in `cartUpdateLines` / `cartRemoveLines`.
1355
1370
  """
@@ -1664,6 +1679,17 @@ type CartShippingMethod {
1664
1679
  title: String!
1665
1680
  }
1666
1681
 
1682
+ """
1683
+ Cart lifecycle status. `ACTIVE` is the only editable state — every other value is terminal and rejects mutations with `CartErrorCode.ALREADY_COMPLETED`. `CONVERTED` carries an associated `completedOrder`; query that to redirect the buyer to their order confirmation. `EXPIRED` / `ABANDONED` are cleanup states with no order; create a fresh cart. `RECOVERED` is a previously-abandoned cart that the buyer returned to via a recovery link.
1684
+ """
1685
+ enum CartStatus {
1686
+ ABANDONED
1687
+ ACTIVE
1688
+ CONVERTED
1689
+ EXPIRED
1690
+ RECOVERED
1691
+ }
1692
+
1667
1693
  """Result of `cartUpdateAttributes`."""
1668
1694
  type CartUpdateAttributesPayload {
1669
1695
  """The updated cart on success."""
@@ -3004,6 +3030,22 @@ enum GiftCardErrorCode {
3004
3030
  NOT_FOUND
3005
3031
  }
3006
3032
 
3033
+ """
3034
+ Recipient details attached to a `GIFT_CARD` line — set with `cartUpdateGiftCardRecipient`. All fields are nullable individually so a partial set (e.g. email only) is representable.
3035
+ """
3036
+ type GiftCardLineRecipient {
3037
+ """Personalised message included with the gift card."""
3038
+ message: String
3039
+
3040
+ """
3041
+ Recipient email — where the gift card code will be delivered on order completion.
3042
+ """
3043
+ recipientEmail: String
3044
+
3045
+ """Recipient display name shown in the gift card email."""
3046
+ recipientName: String
3047
+ }
3048
+
3007
3049
  """Gift card status"""
3008
3050
  enum GiftCardStatus {
3009
3051
  ACTIVE