@asstio-dev/storefront 0.6.0 → 0.9.2
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/README.md +187 -4
- package/dist/checkout/browser.d.ts +76 -0
- package/dist/checkout/browser.js +444 -0
- package/dist/checkout/controllers.d.ts +55 -0
- package/dist/checkout/controllers.js +75 -0
- package/dist/checkout/server.d.ts +37 -0
- package/dist/checkout/server.js +68 -0
- package/dist/checkout/types.d.ts +42 -0
- package/dist/checkout/types.js +17 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/media.d.ts +80 -0
- package/dist/media.js +142 -0
- package/dist/schema.d.ts +735 -25
- package/package.json +4 -2
- package/src/schema.d.ts +735 -25
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ const { data: page } = await storefront.GET("/v1/products/{slugOrId}/variants",
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Every example in this file is compiled as part of `npm run check` — the source of them is
|
|
56
|
-
[`examples/readme.ts`](examples/readme.ts), which also covers product lookup, ETag revalidation and
|
|
56
|
+
[`examples/readme.ts`](examples/readme.ts), which also covers product lookup, ETag revalidation, crawling, and image formats.
|
|
57
57
|
|
|
58
58
|
### Images
|
|
59
59
|
|
|
@@ -78,6 +78,70 @@ for (const file of product?.files ?? []) console.log(file.filename, fileUrl(site
|
|
|
78
78
|
`videoSources` returns `[]` for anything that is not a video, while `mediaUrl`/`pictureSources`/`fileUrl`/`posterUrl`
|
|
79
79
|
throw `MediaUrlError` when the descriptor cannot produce that URL — branch on `kind` before calling.
|
|
80
80
|
|
|
81
|
+
### Image formats
|
|
82
|
+
|
|
83
|
+
A site's **formats** (`site.media.formats`: `card`, `gallery`, `thumb`, `cart`, `swatch`, `default`, plus custom
|
|
84
|
+
keys) say how an image is framed: aspect, fit (`contain` · `cover` · `natural`), padding and a background per
|
|
85
|
+
`bg` class. They are published with the site, so the look changes without a frontend deploy. `formatImage` turns
|
|
86
|
+
a descriptor and a format key into everything a `<picture>` needs:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { formatImage } from "@asstio-dev/storefront";
|
|
90
|
+
|
|
91
|
+
const f = formatImage(site.media, product.image, "card", [
|
|
92
|
+
{ media: "(min-width: 1080px)", size: "25vw" }, // your slot's width per breakpoint
|
|
93
|
+
{ size: "100vw" }, // the last one without a media condition
|
|
94
|
+
]);
|
|
95
|
+
// React:
|
|
96
|
+
// <div className={styles.cardImage}> ← your slot: your class, may be a flex/grid item
|
|
97
|
+
// <div style={f.box}> ← the aspect frame, the slot's only child
|
|
98
|
+
// <canvas ref={blurhashCanvas} style={f.placeholder} /> ← optional placeholder, same rectangle as the image
|
|
99
|
+
// <picture>
|
|
100
|
+
// {f.sources.map((s) => <source key={s.type} {...s} />)}
|
|
101
|
+
// <img {...f.fallback} style={f.img} alt={alt} loading="lazy" />
|
|
102
|
+
// </picture>
|
|
103
|
+
// </div>
|
|
104
|
+
// </div>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Plain HTML (templating):
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const f = formatImage(site.media, product.image, "card", [
|
|
111
|
+
{ media: "(min-width: 1080px)", size: "25vw" },
|
|
112
|
+
{ media: "(min-width: 640px)", size: "50vw" },
|
|
113
|
+
{ size: "100vw" },
|
|
114
|
+
]);
|
|
115
|
+
// alt is merchant text, not SDK output: escape it for the attribute.
|
|
116
|
+
const escapeAttr = (s: string) => s.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
117
|
+
const alt = escapeAttr(product.image.alt["sv"] ?? product.name);
|
|
118
|
+
// Your slot owns the width; box is its only child in every fit:
|
|
119
|
+
const picture =
|
|
120
|
+
`<picture>${f.sources.map((s) => `<source type="${s.type}" srcset="${s.srcSet}" sizes="${s.sizes}">`).join("")}` +
|
|
121
|
+
`<img src="${f.fallback.src}" srcset="${f.fallback.srcSet}" sizes="${f.fallback.sizes}" width="${f.fallback.width}" height="${f.fallback.height}" alt="${alt}" loading="lazy" style="${styleToCss(f.img)}"></picture>`;
|
|
122
|
+
const placeholder = `<canvas width="32" height="32" style="${styleToCss(f.placeholder)}"></canvas>`;
|
|
123
|
+
const html = `<div class="card-image"><div style="${styleToCss(f.box)}">${placeholder}${picture}</div></div>`;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **Element contract.** Your slot element owns the width (grid, flex, `className`); it may be stretched. Put the
|
|
127
|
+
`box` inside it as its only child and give the box **no** padding, border or height of its own — its inline
|
|
128
|
+
styles pin its geometry, so a stretched slot never distorts the format's aspect.
|
|
129
|
+
- **One rectangle.** `img` and `placeholder` are the same absolutely positioned rectangle with `object-fit: fill`,
|
|
130
|
+
computed from the descriptor: a blurhash canvas of any decode size is stretched to exactly where the image will
|
|
131
|
+
paint. The box's `backgroundColor` shows around a `contain` image and through transparent AVIF/WebP.
|
|
132
|
+
- **Natural** has a box too, at the image's own aspect and without a colour; `img` and `placeholder` fill it. So
|
|
133
|
+
the layout is reserved before load in every fit, and the placeholder sits under the image the same way.
|
|
134
|
+
- **`sizes`** is your slots scaled by the image's drawn fraction with `calc()`; `srcset` keeps the real widths, so
|
|
135
|
+
the browser downloads a sharp enough rendition for a cover crop and not a wasted one for a padded packshot.
|
|
136
|
+
- `formats: null` (a site without formats) or an unknown key falls back to `default`, then to `natural`; your own
|
|
137
|
+
CSS keeps working. `focal` (set per placement in Asstio) positions a `cover` crop; `showWhole` renders a cover
|
|
138
|
+
format as contain for that image. `formatImage` throws `MediaUrlError` for video descriptors, file descriptors,
|
|
139
|
+
a descriptor without `w`/`h`, a descriptor without renditions, or an empty `slots` array.
|
|
140
|
+
- Plain HTML: see the templating example above, or the compiled function `productCard` in [`examples/readme.ts`](examples/readme.ts).
|
|
141
|
+
`styleToCss(f.box)` gives the `style` attribute value for any element. Escape anything that is not SDK output
|
|
142
|
+
(`alt`, product names) before it goes into markup; the React example needs no escaping because React escapes
|
|
143
|
+
attribute values.
|
|
144
|
+
|
|
81
145
|
### Product content
|
|
82
146
|
|
|
83
147
|
`description` is **already sanitized** on the server: render `description.html` as-is — do not sanitize, escape or
|
|
@@ -129,6 +193,120 @@ case-insensitively, or key prefix — returning at most 50. A facet with more di
|
|
|
129
193
|
over its top 1000; no regular expression ever reaches the search index. `key` has to be a `list` or `swatch` facet:
|
|
130
194
|
an unknown key is `FACET_NOT_FOUND` (404), and a range or toggle facet is `FACET_NOT_SEARCHABLE` (400).
|
|
131
195
|
|
|
196
|
+
### Checkout
|
|
197
|
+
|
|
198
|
+
Two small modules, because the suspend → update → resume rule spans two runtimes. The server helpers need the
|
|
199
|
+
client and the cart token (an `httpOnly` cookie, so they run in Server Actions and route handlers);
|
|
200
|
+
`mountCheckout` needs neither key nor fetch — it mounts the provider's widget and drives it from the answers
|
|
201
|
+
your server hands it.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { awaitCompletion, completeCheckout, getCheckoutStatus, getOrder, mountCheckout, startCheckout, syncCheckout } from "@asstio-dev/storefront";
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Server.** `startCheckout(client, cartToken, { option?, returnOrigin? })` creates the cart's checkout attempt
|
|
208
|
+
or converges on the live one; call it from a Server Action or route handler **on the shopper's navigation to the
|
|
209
|
+
checkout page, never during a Server Component's render** (a re-render would create a provider order per
|
|
210
|
+
mutation). `syncCheckout` is the same `POST /v1/checkout`, retrying `CHECKOUT_IN_PROGRESS` per `Retry-After`
|
|
211
|
+
twice by default — **call it after every cart mutation on the checkout page**, through the browser handle's
|
|
212
|
+
`update`; there, and for the handle's `sync`, pass `{ retries: 0 }` so each call is one POST and the handle's own
|
|
213
|
+
burst does the retrying. `getCheckoutStatus(client, attemptId, cartToken)` is the non-mutating
|
|
214
|
+
`GET /v1/checkout/{attemptId}` behind the handle's `status` callback.
|
|
215
|
+
`completeCheckout(client, attemptId, cartToken?)` is the confirmation page's call; with the cart token
|
|
216
|
+
the answer carries `order.accessToken`, on every call, so a lost first answer or a reload still ends with the token.
|
|
217
|
+
`awaitCompletion(() => completeCheckout(…))` polls with backoff until the order exists. `getOrder(client, orderId,
|
|
218
|
+
token)` sends the token as `X-Order-Token` — a header, never a query string; send `Referrer-Policy: no-referrer`
|
|
219
|
+
on the order page and keep its URL out of your logs.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// A Server Action or route handler, on the shopper's navigation to the checkout page.
|
|
223
|
+
const result = await startCheckout(storefront, cartToken, { returnOrigin: "https://shop.example" });
|
|
224
|
+
if (!result.ok) return { problem: result.problem, retryAfter: result.retryAfter };
|
|
225
|
+
return { checkout: result.data }; // "open" with a widget, or "completed"/"pending" with an order
|
|
226
|
+
|
|
227
|
+
// The Server Action behind mountCheckout's `sync`, and the POST inside every cart update: one HTTP POST per call.
|
|
228
|
+
export async function syncForBrowser() {
|
|
229
|
+
return syncCheckout(storefront, cartToken, undefined, { retries: 0 });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The confirmation page: complete with the cart token, keep the order's access token, then poll for the number.
|
|
233
|
+
const first = await completeCheckout(storefront, attemptId, cartToken);
|
|
234
|
+
const done = await awaitCompletion(() => completeCheckout(storefront, attemptId, cartToken));
|
|
235
|
+
|
|
236
|
+
// The order page: the token is a header.
|
|
237
|
+
const order = await getOrder(storefront, orderId, token); // 404 ORDER_NOT_FOUND for an unknown id and a wrong token alike
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Browser.** `mountCheckout(container, checkout, { sync, status, onProblem, onOrder })` takes the `open` answer
|
|
241
|
+
your page was rendered with and returns a handle `{ attemptId, suspended, suspend, resume, remount, destroy,
|
|
242
|
+
sync(update) }`. The container is a client component **keyed by `attemptId`**. `handle.sync(update)` is the whole
|
|
243
|
+
rule: it suspends the widget, runs your `update` **once** (mutate the cart, `POST /checkout`, return its answer),
|
|
244
|
+
then dispatches **status first, identity on `open`**: `completed`/`pending` → `onOrder(order, attemptId)` (you
|
|
245
|
+
navigate); `locked` → stays suspended and retries `sync()` per `Retry-After` within the API's budget (30 s, ten
|
|
246
|
+
calls — a 429 counts the same; a `Retry-After` past the 30 s is not waited out), then polls `status(attemptId)`;
|
|
247
|
+
`processing` → polls `status()`; `declined`/`expired` → one `sync()`; `open` → resumes when the answer's
|
|
248
|
+
`attemptId` is the mounted one, else remounts from the answer's widget (`changed` only says *this* call created a
|
|
249
|
+
new attempt — never resume on it). A `status()` answer is never dispatched as payable: its `open` leads to one
|
|
250
|
+
non-mutating `sync()`. An `update` that throws leaves the widget suspended. The widget is suspended the moment
|
|
251
|
+
`handle.sync` is called, and stays suspended while another update is queued behind it; a newly queued update ends
|
|
252
|
+
the running sync's waits (a retry or a status poll), so it runs at once. `CHECKOUT_IN_PROGRESS` is retried like
|
|
253
|
+
`locked`, within the same burst. If the provider's script loads late, the handle keeps trying to bind its decline
|
|
254
|
+
signal every half second for as long as the widget is mounted. A 503 `CHECKOUT_UNAVAILABLE` or
|
|
255
|
+
`CHECKOUT_PROVIDER_UNAVAILABLE` is retried per its `Retry-After` by non-mutating syncs within the same budget, then
|
|
256
|
+
reported; any other 503 (`SITE_KEYS_SYNCING`, `SITE_REGISTRY_WARMING`) and every other non-2xx of the SDK's own calls
|
|
257
|
+
reaches `onProblem(problem, { status })` at once, `status` being the HTTP status — a `409 CHECKOUT_CART_CHANGED`
|
|
258
|
+
carries the corrected cart in `problem.cart`: show it and its warnings, then `handle.sync(() => POST /checkout)`.
|
|
259
|
+
`handle.sync` never rejects. `onProblem` also receives the SDK's own codes: `SDK_SYNC_FAILED` or `SDK_STATUS_FAILED`
|
|
260
|
+
when one of its calls throws (a network failure), `SDK_UPDATE_FAILED` when your `update` throws, and
|
|
261
|
+
`SDK_DISPATCH_FAILED` for any other throw while dispatching (an `onOrder` that throws, an unknown widget controller) —
|
|
262
|
+
each with `{ status: 0 }` and the widget left suspended; `SDK_CHECKOUT_CLOSED` when the one `sync()` after a
|
|
263
|
+
`declined`/`expired` answers closed again, and `SDK_UNKNOWN_STATUS` for an answer status the SDK does not know (both
|
|
264
|
+
`{ status: 200 }`); and `SDK_EMPTY_ANSWER`, which the server helpers return when an answer carried no body (with that
|
|
265
|
+
answer's HTTP status). The SDK also syncs **by itself**: once on the provider's
|
|
266
|
+
payment-declined signal where its JavaScript offers one, when the page becomes visible again while suspended, and
|
|
267
|
+
**three minutes before expiry, timed from the DTO's `expiresInSeconds`** (server-relative, so a browser clock ahead
|
|
268
|
+
of the server's still lands inside the server's five-minute window). An autonomous sync is skipped while another
|
|
269
|
+
sync is running or queued.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
// A client component, keyed by attemptId so a re-render never resets it.
|
|
273
|
+
const handle = mountCheckout(container, checkout, {
|
|
274
|
+
sync: callSync, // a Server Action calling syncForBrowser()
|
|
275
|
+
status: callStatus, // a Server Action calling getCheckoutStatus(storefront, attemptId, cartToken)
|
|
276
|
+
onProblem: (problem) => { /* problem.cart when code === "CHECKOUT_CART_CHANGED"; otherwise the message for problem.code */ },
|
|
277
|
+
onOrder: (order, attemptId) => { location.assign(`/kassa/tack?attempt=${attemptId}&order=${order.id}`); },
|
|
278
|
+
});
|
|
279
|
+
// Every cart mutation on the page goes through the handle: suspend → your update once → status first, identity on open.
|
|
280
|
+
await handle.sync(() => callAddLine(sku)); // callAddLine mutates the cart, then syncs, and returns that answer
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**After completion.** Every cart endpoint answers `410 CART_EXPIRED` for the converted cart, while the cart token
|
|
284
|
+
is still what re-delivers the order's access token — so **a `410` from a cart mutation on the checkout or
|
|
285
|
+
confirmation path does not end the cart cookie**: call `GET /v1/checkout/{attemptId}` (or `POST /checkout`, which
|
|
286
|
+
answers the converted cart's completed attempt) with the same token, navigate to the confirmation from its
|
|
287
|
+
`order`, and clear the cookie only once the confirmation page holds the access token.
|
|
288
|
+
|
|
289
|
+
**Copy and CSP.** The `pending` state ("awaiting payment approval") is your copy. Each provider's widget needs its
|
|
290
|
+
domains in your CSP: Qliro One `*.qliro.com` (`pago.qit.nu` in the sandbox), Klarna/Kustom `*.klarna.com`
|
|
291
|
+
(`*.kustom.co`), Walley `*.walleypay.com`, plus their `frame-src`/`script-src`/`connect-src` — the provider's
|
|
292
|
+
integration guide lists the exact hosts.
|
|
293
|
+
|
|
294
|
+
Problem codes you will meet here: `CART_EMPTY`, `CHECKOUT_CART_CHANGED` (with `cart`), `CHECKOUT_PREVIEW_KEY`,
|
|
295
|
+
`CHECKOUT_MARKET_MISMATCH` (switch the cart's market first, then sync), `OPTION_NOT_AVAILABLE`,
|
|
296
|
+
`ORIGIN_NOT_ALLOWED`, `CHECKOUT_UNAVAILABLE` and `CHECKOUT_PROVIDER_UNAVAILABLE` (503, `Retry-After`),
|
|
297
|
+
`CHECKOUT_IN_PROGRESS` (409, `Retry-After`), `CHECKOUT_PROVIDER_REJECTED` (502), `CHECKOUT_NOT_FOUND`,
|
|
298
|
+
`ORDER_NOT_FOUND`, `SITE_KEYS_SYNCING` (503 for a secret key on a freshly created site, `Retry-After`).
|
|
299
|
+
|
|
300
|
+
#### Qliro (`qliro-q1`)
|
|
301
|
+
|
|
302
|
+
- The widget is Qliro's HTML snippet; `mountCheckout` re-creates its scripts so they run.
|
|
303
|
+
- After every cart change, `handle.sync(update)` locks the widget (`q1.lock()`), and resumes it through
|
|
304
|
+
`q1.onOrderUpdated`, which makes the iframe re-read the order. The widget unlocks only when Qliro's order shows the
|
|
305
|
+
confirmed total. If it does not within 10 seconds, or the change came before Qliro's script had loaded, the widget is
|
|
306
|
+
re-rendered from the confirmed checkout instead; it is never unlocked before Qliro shows the confirmed total.
|
|
307
|
+
- When our validation declines a purchase (the cart moved on, the attempt expired, a line is out of stock), Qliro shows
|
|
308
|
+
its out-of-stock message and fires `q1.onPaymentDeclined`; the SDK then syncs by itself and remounts or resumes.
|
|
309
|
+
|
|
132
310
|
## Headers
|
|
133
311
|
|
|
134
312
|
Every call the client makes carries these; you never set them by hand.
|
|
@@ -170,9 +348,14 @@ Codes you will meet: `SITE_KEY_MISSING`, `SITE_KEY_INVALID`, `SITE_INACTIVE`, `S
|
|
|
170
348
|
`SITE_REGISTRY_WARMING`, `MARKET_UNKNOWN`, `RATE_LIMITED` (with `retryAfterSeconds`, plus a `Retry-After`
|
|
171
349
|
header), `PRODUCT_NOT_FOUND`, `COLLECTION_NOT_FOUND`, `INVALID_PAGE`, `INVALID_SINCE`, `INVALID_TYPE`,
|
|
172
350
|
`INDEX_UNAVAILABLE` (retry — a preview index is being rebuilt), `NOT_FOUND`, `INVALID_QUERY`,
|
|
173
|
-
`FACET_NOT_FOUND`, `FACET_NOT_SEARCHABLE
|
|
174
|
-
|
|
175
|
-
`
|
|
351
|
+
`FACET_NOT_FOUND`, `FACET_NOT_SEARCHABLE`,
|
|
352
|
+
and, from the checkout ([above](#checkout)): `CART_EMPTY`, `CHECKOUT_CART_CHANGED`, `CHECKOUT_PREVIEW_KEY`,
|
|
353
|
+
`CHECKOUT_MARKET_MISMATCH`, `OPTION_NOT_AVAILABLE`, `ORIGIN_NOT_ALLOWED`, `CHECKOUT_UNAVAILABLE`,
|
|
354
|
+
`CHECKOUT_PROVIDER_UNAVAILABLE`, `CHECKOUT_IN_PROGRESS`, `CHECKOUT_PROVIDER_REJECTED`, `CHECKOUT_NOT_FOUND`,
|
|
355
|
+
`ORDER_NOT_FOUND`, `SITE_KEYS_SYNCING`, `CART_EXPIRED`.
|
|
356
|
+
|
|
357
|
+
`SITE_REGISTRY_WARMING` and `INDEX_UNAVAILABLE` are retries, not bugs in your call, as are the checkout's
|
|
358
|
+
`CHECKOUT_IN_PROGRESS`, `CHECKOUT_UNAVAILABLE`, `CHECKOUT_PROVIDER_UNAVAILABLE` and `SITE_KEYS_SYNCING` (each with `Retry-After`).
|
|
176
359
|
|
|
177
360
|
## Warnings
|
|
178
361
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ProviderWindow } from "./controllers.js";
|
|
2
|
+
import { type CheckoutDto, type CheckoutOrderRefDto, type CheckoutResult, type StatusResult, type StorefrontProblem } from "./types.js";
|
|
3
|
+
export interface MinimalScript {
|
|
4
|
+
src: string;
|
|
5
|
+
textContent: string | null;
|
|
6
|
+
async: boolean;
|
|
7
|
+
defer: boolean;
|
|
8
|
+
type: string;
|
|
9
|
+
getAttributeNames(): string[];
|
|
10
|
+
getAttribute(name: string): string | null;
|
|
11
|
+
setAttribute(name: string, value: string): void;
|
|
12
|
+
parentNode?: {
|
|
13
|
+
replaceChild(newNode: unknown, oldNode: unknown): void;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
export interface MinimalElement {
|
|
17
|
+
innerHTML: string;
|
|
18
|
+
querySelectorAll(selector: "script"): ArrayLike<MinimalScript>;
|
|
19
|
+
appendChild(node: unknown): unknown;
|
|
20
|
+
ownerDocument?: MinimalDocument | null;
|
|
21
|
+
}
|
|
22
|
+
export interface MinimalDocument {
|
|
23
|
+
createElement(tag: "script"): MinimalScript;
|
|
24
|
+
visibilityState: string;
|
|
25
|
+
addEventListener(type: "visibilitychange", fn: () => void): void;
|
|
26
|
+
removeEventListener(type: "visibilitychange", fn: () => void): void;
|
|
27
|
+
}
|
|
28
|
+
export interface MinimalWindow {
|
|
29
|
+
document?: MinimalDocument;
|
|
30
|
+
}
|
|
31
|
+
export interface MountOptions {
|
|
32
|
+
/** A plain, non-mutating `POST /checkout` (server-side, with the cart token). Recommended: one HTTP POST per call,
|
|
33
|
+
* `syncCheckout(client, cartToken, opts, { retries: 0 })` — the handle retries `CHECKOUT_IN_PROGRESS` itself inside
|
|
34
|
+
* its 30 s burst *(plan review 3 #5, final review S1)*. */
|
|
35
|
+
sync: () => Promise<CheckoutResult>;
|
|
36
|
+
/** A `GET /checkout/{attemptId}` on the attempt named. */
|
|
37
|
+
status: (attemptId: string) => Promise<StatusResult>;
|
|
38
|
+
/** Every non-2xx the SDK's own calls meet, a call of its own that threw, an `update` that threw, and any other
|
|
39
|
+
* throw while dispatching (`SDK_DISPATCH_FAILED`: an `onOrder` that threw, an unknown controller). A 409
|
|
40
|
+
* CHECKOUT_CART_CHANGED carries the corrected cart in `problem.cart`. */
|
|
41
|
+
onProblem: (problem: StorefrontProblem, context: {
|
|
42
|
+
status: number;
|
|
43
|
+
}) => void;
|
|
44
|
+
/** An order exists: navigate to the confirmation from it. The SDK never navigates itself. */
|
|
45
|
+
onOrder: (order: CheckoutOrderRefDto, attemptId: string) => void;
|
|
46
|
+
document?: MinimalDocument;
|
|
47
|
+
window?: ProviderWindow & MinimalWindow;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
50
|
+
clearTimeout?: (handle: unknown) => void;
|
|
51
|
+
/** The `locked` burst, sized to the API's checkout:{cartToken} policy (20/min): 30 s and at most ten calls (spec
|
|
52
|
+
* §10.3). The spec's burst is the whole client-side budget: the rolling-minute POST ledger rounds 2–3 added is cut —
|
|
53
|
+
* the server enforces the policy and a 429 is handled like `locked` *(final review S1)*. */
|
|
54
|
+
lockedBudgetMs?: number;
|
|
55
|
+
lockedMaxCalls?: number;
|
|
56
|
+
pollFirstMs?: number;
|
|
57
|
+
pollMaxMs?: number;
|
|
58
|
+
/** How many consecutive `locked` status answers lead to one sync() rather than more polling *(plan review 1 #12)*. */
|
|
59
|
+
lockedPollsBeforeSync?: number;
|
|
60
|
+
/** Binding the provider's decline signal until its script has run: every `bindRetryMs` (500 ms) for as long as the
|
|
61
|
+
* widget is mounted — one interval, no fast and slow phases *(plan review 2 #13, plan review 3 #4, final review S8)*. */
|
|
62
|
+
bindRetryMs?: number;
|
|
63
|
+
/** How long before `expiresInSeconds` the SDK syncs by itself (inside the server's five-minute window). */
|
|
64
|
+
preExpiryMs?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface CheckoutHandle {
|
|
67
|
+
readonly attemptId: string;
|
|
68
|
+
readonly suspended: boolean;
|
|
69
|
+
suspend(): void;
|
|
70
|
+
resume(): void;
|
|
71
|
+
remount(checkout: CheckoutDto): void;
|
|
72
|
+
destroy(): void;
|
|
73
|
+
/** The whole rule: suspend → `await update()` once → status first, identity on `open`. Never rejects. */
|
|
74
|
+
sync(update: () => Promise<CheckoutResult>): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
export declare function mountCheckout(container: MinimalElement, checkout: CheckoutDto, options: MountOptions): CheckoutHandle;
|