@asstio-dev/storefront 0.7.0 → 0.11.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/README.md +198 -6
- 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 +9 -0
- package/dist/index.js +9 -0
- package/dist/schema.d.ts +1029 -75
- package/dist/seo/breadcrumb.d.ts +9 -0
- package/dist/seo/breadcrumb.js +18 -0
- package/dist/seo/head.d.ts +10 -0
- package/dist/seo/head.js +101 -0
- package/dist/seo/locales.d.ts +26 -0
- package/dist/seo/locales.js +95 -0
- package/dist/seo/product.d.ts +6 -0
- package/dist/seo/product.js +128 -0
- package/dist/seo/serialize.d.ts +6 -0
- package/dist/seo/serialize.js +8 -0
- package/dist/seo/shared.d.ts +38 -0
- package/dist/seo/shared.js +106 -0
- package/dist/seo/types.d.ts +100 -0
- package/dist/seo/types.js +7 -0
- package/package.json +6 -2
- package/src/schema.d.ts +1029 -75
package/README.md
CHANGED
|
@@ -146,9 +146,11 @@ const html = `<div class="card-image"><div style="${styleToCss(f.box)}">${placeh
|
|
|
146
146
|
|
|
147
147
|
`description` is **already sanitized** on the server: render `description.html` as-is — do not sanitize, escape or
|
|
148
148
|
re-parse it — and use `description.text` wherever you need plain text (a meta description, a snippet, an
|
|
149
|
-
`aria-label`). `shortDescription` is plain text. `brand
|
|
150
|
-
|
|
151
|
-
filter.
|
|
149
|
+
`aria-label`). `shortDescription` is plain text. `brand` is the product's brand (the main product's); every variant in `variants[]` and on `/variants` carries its own
|
|
150
|
+
`brand` and `responsible` — render the **selected variant's**. `brand.facet` links a filtered listing
|
|
151
|
+
(`filter[facet.key]=facet.valueKey`) when the shop has a brand filter. `brand.url` is the brand page on this market and
|
|
152
|
+
is `null` whenever that page would not exist here (brand pages off, or nothing of the brand sold on this market) — then
|
|
153
|
+
render the label (and `brand.logo`) without a link; never build a brand URL yourself.
|
|
152
154
|
|
|
153
155
|
`attributes[]` arrives ordered and labelled for display. Branch on `type` (`text` | `html` | `number` | `bool` |
|
|
154
156
|
`list`): a list fills `values`, everything else fills `value`, and `html` fills `html` too — sanitized exactly like
|
|
@@ -170,6 +172,77 @@ value has no key of its own: a list is either fully linkable or not linkable at
|
|
|
170
172
|
`related[]` holds groups of cards already resolved for this market, and `bundle.components[]` the cards a bundle is
|
|
171
173
|
made of; both leave out anything that no longer exists or is not sold here.
|
|
172
174
|
|
|
175
|
+
### Brands and responsible parties
|
|
176
|
+
|
|
177
|
+
`responsible` is `{ manufacturer, importer, euResponsiblePerson }`, each a party or `null`. Show the selected variant's
|
|
178
|
+
on the product page (EU GPSR manufacturer and responsible-person contact data). `null` means nothing applies for this
|
|
179
|
+
SKU: do not fall back to the product's or the brand's. `phone` is only present when the shop publishes it.
|
|
180
|
+
|
|
181
|
+
Brand pages: `GET /v1/brands` lists the brands with a page on this market (paged, by name); `GET /v1/brands/{slugOrId}`
|
|
182
|
+
returns the brand's name, description (sanitized like product descriptions), logo, website, its default parties and
|
|
183
|
+
`facet` — render the listing with `GET /v1/products?filter[<facet.key>]=<facet.valueKey>`. A `redirectTo` means 301 to
|
|
184
|
+
that slug. `GET /v1/sitemap?type=brand` lists them for `sitemap.xml`.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const { data: brand } = await storefront.GET("/v1/brands/{slugOrId}", { params: { path: { slugOrId: "hugo-boss" } } });
|
|
188
|
+
if (brand?.redirectTo) redirect(301, `/varumarken/${brand.redirectTo}`);
|
|
189
|
+
const { data: listing } = await storefront.GET("/v1/products", {
|
|
190
|
+
params: { query: { filter: filterQuery({ [brand!.facet!.key]: [brand!.facet!.valueKey] }) } },
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### SEO
|
|
195
|
+
|
|
196
|
+
Pure helpers that compute schema.org JSON-LD and `<head>` data from the DTOs. They render nothing: your frontend
|
|
197
|
+
writes the markup. No backend call, no DOM, no dependency.
|
|
198
|
+
|
|
199
|
+
**Product JSON-LD.** `productJsonLd` returns a `Product` (one variant) or a `ProductGroup` with one `Product` and
|
|
200
|
+
`Offer` per variant sold on the market. `variantUrl` is required: Google needs a distinct URL per variant, and opening
|
|
201
|
+
that URL **must preselect the variant** on your page. The format is yours; `?variant=` below is only an example.
|
|
202
|
+
`variesBy` maps your option names to `size`, `color`, `material` or `pattern`; unmapped options still name the
|
|
203
|
+
variants. A product with more than 100 variants arrives with `variants: null`: pass the first `/variants` page as
|
|
204
|
+
`variants`, or the product gets no offer (valid schema.org, but not eligible for product rich results).
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
const ld = productJsonLd(product, {
|
|
208
|
+
site, market: "se",
|
|
209
|
+
variantUrl: (v) => `${product.url}?variant=${v.id}`,
|
|
210
|
+
variesBy: { Storlek: "size", Färg: "color" },
|
|
211
|
+
});
|
|
212
|
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(ld) }} />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
In a plain template: `<script type="application/ld+json">${jsonLdScript(ld)}</script>`. `jsonLdScript` escapes
|
|
216
|
+
`<`, `>`, `&`, U+2028 and U+2029, so merchant text cannot end the script element; never use plain `JSON.stringify` there.
|
|
217
|
+
|
|
218
|
+
**Breadcrumbs.** On a collection page, `r.breadcrumb` holds the ancestors only. Add the collection itself as `current`:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const crumbs = breadcrumbJsonLd(r.breadcrumb, { home: { name: "Hem", url: "https://moory.se/" }, current: r.collection });
|
|
222
|
+
// null below two items (Google's minimum): render nothing then.
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Head.** `productHead`, `collectionHead` (takes your own `description`; the API has none for collections) and
|
|
226
|
+
`brandHead` return a framework-neutral `SeoHead`: title, description (cut at 160 characters on a word boundary),
|
|
227
|
+
canonical, hreflang alternates (keyed `language-COUNTRY`, `x-default` from the site's default market; override with
|
|
228
|
+
`xDefault: "<market>"` or `false`), and Open Graph / Twitter meta. `titleTemplate: "%s | Moory"` applies to `<title>`
|
|
229
|
+
only, never to the social titles. In Next.js:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
233
|
+
return toNextMetadata(productHead(product, { site, market: "se", titleTemplate: "%s | Moory" }));
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Titles come out as `{ absolute }` on purpose, so a layout's `title.template` does not apply a second time.
|
|
238
|
+
|
|
239
|
+
**Errors.** `SeoError` means a caller or configuration error: `redirectTo` is set (answer 301 instead), an unknown
|
|
240
|
+
market, a bad `variesBy` value, a `variantUrl` that is not an absolute http(s) URL or repeats for two variants, or
|
|
241
|
+
two markets with the same `language-COUNTRY` and different URLs. Catch it at your SEO boundary and fix the market
|
|
242
|
+
config. Holes in the data never throw: a missing image, description or price only leaves that part out.
|
|
243
|
+
|
|
244
|
+
hreflang sets are only as reciprocal as the API's `alternateUrls`; the SDK renders what the API gives.
|
|
245
|
+
|
|
173
246
|
### Search-as-you-type and search-in-facet
|
|
174
247
|
|
|
175
248
|
`/v1/search/suggest` answers three short lists for what the shopper has typed so far: up to 5 product cards
|
|
@@ -193,6 +266,120 @@ case-insensitively, or key prefix — returning at most 50. A facet with more di
|
|
|
193
266
|
over its top 1000; no regular expression ever reaches the search index. `key` has to be a `list` or `swatch` facet:
|
|
194
267
|
an unknown key is `FACET_NOT_FOUND` (404), and a range or toggle facet is `FACET_NOT_SEARCHABLE` (400).
|
|
195
268
|
|
|
269
|
+
### Checkout
|
|
270
|
+
|
|
271
|
+
Two small modules, because the suspend → update → resume rule spans two runtimes. The server helpers need the
|
|
272
|
+
client and the cart token (an `httpOnly` cookie, so they run in Server Actions and route handlers);
|
|
273
|
+
`mountCheckout` needs neither key nor fetch — it mounts the provider's widget and drives it from the answers
|
|
274
|
+
your server hands it.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { awaitCompletion, completeCheckout, getCheckoutStatus, getOrder, mountCheckout, startCheckout, syncCheckout } from "@asstio-dev/storefront";
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Server.** `startCheckout(client, cartToken, { option?, returnOrigin? })` creates the cart's checkout attempt
|
|
281
|
+
or converges on the live one; call it from a Server Action or route handler **on the shopper's navigation to the
|
|
282
|
+
checkout page, never during a Server Component's render** (a re-render would create a provider order per
|
|
283
|
+
mutation). `syncCheckout` is the same `POST /v1/checkout`, retrying `CHECKOUT_IN_PROGRESS` per `Retry-After`
|
|
284
|
+
twice by default — **call it after every cart mutation on the checkout page**, through the browser handle's
|
|
285
|
+
`update`; there, and for the handle's `sync`, pass `{ retries: 0 }` so each call is one POST and the handle's own
|
|
286
|
+
burst does the retrying. `getCheckoutStatus(client, attemptId, cartToken)` is the non-mutating
|
|
287
|
+
`GET /v1/checkout/{attemptId}` behind the handle's `status` callback.
|
|
288
|
+
`completeCheckout(client, attemptId, cartToken?)` is the confirmation page's call; with the cart token
|
|
289
|
+
the answer carries `order.accessToken`, on every call, so a lost first answer or a reload still ends with the token.
|
|
290
|
+
`awaitCompletion(() => completeCheckout(…))` polls with backoff until the order exists. `getOrder(client, orderId,
|
|
291
|
+
token)` sends the token as `X-Order-Token` — a header, never a query string; send `Referrer-Policy: no-referrer`
|
|
292
|
+
on the order page and keep its URL out of your logs.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// A Server Action or route handler, on the shopper's navigation to the checkout page.
|
|
296
|
+
const result = await startCheckout(storefront, cartToken, { returnOrigin: "https://shop.example" });
|
|
297
|
+
if (!result.ok) return { problem: result.problem, retryAfter: result.retryAfter };
|
|
298
|
+
return { checkout: result.data }; // "open" with a widget, or "completed"/"pending" with an order
|
|
299
|
+
|
|
300
|
+
// The Server Action behind mountCheckout's `sync`, and the POST inside every cart update: one HTTP POST per call.
|
|
301
|
+
export async function syncForBrowser() {
|
|
302
|
+
return syncCheckout(storefront, cartToken, undefined, { retries: 0 });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The confirmation page: complete with the cart token, keep the order's access token, then poll for the number.
|
|
306
|
+
const first = await completeCheckout(storefront, attemptId, cartToken);
|
|
307
|
+
const done = await awaitCompletion(() => completeCheckout(storefront, attemptId, cartToken));
|
|
308
|
+
|
|
309
|
+
// The order page: the token is a header.
|
|
310
|
+
const order = await getOrder(storefront, orderId, token); // 404 ORDER_NOT_FOUND for an unknown id and a wrong token alike
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Browser.** `mountCheckout(container, checkout, { sync, status, onProblem, onOrder })` takes the `open` answer
|
|
314
|
+
your page was rendered with and returns a handle `{ attemptId, suspended, suspend, resume, remount, destroy,
|
|
315
|
+
sync(update) }`. The container is a client component **keyed by `attemptId`**. `handle.sync(update)` is the whole
|
|
316
|
+
rule: it suspends the widget, runs your `update` **once** (mutate the cart, `POST /checkout`, return its answer),
|
|
317
|
+
then dispatches **status first, identity on `open`**: `completed`/`pending` → `onOrder(order, attemptId)` (you
|
|
318
|
+
navigate); `locked` → stays suspended and retries `sync()` per `Retry-After` within the API's budget (30 s, ten
|
|
319
|
+
calls — a 429 counts the same; a `Retry-After` past the 30 s is not waited out), then polls `status(attemptId)`;
|
|
320
|
+
`processing` → polls `status()`; `declined`/`expired` → one `sync()`; `open` → resumes when the answer's
|
|
321
|
+
`attemptId` is the mounted one, else remounts from the answer's widget (`changed` only says *this* call created a
|
|
322
|
+
new attempt — never resume on it). A `status()` answer is never dispatched as payable: its `open` leads to one
|
|
323
|
+
non-mutating `sync()`. An `update` that throws leaves the widget suspended. The widget is suspended the moment
|
|
324
|
+
`handle.sync` is called, and stays suspended while another update is queued behind it; a newly queued update ends
|
|
325
|
+
the running sync's waits (a retry or a status poll), so it runs at once. `CHECKOUT_IN_PROGRESS` is retried like
|
|
326
|
+
`locked`, within the same burst. If the provider's script loads late, the handle keeps trying to bind its decline
|
|
327
|
+
signal every half second for as long as the widget is mounted. A 503 `CHECKOUT_UNAVAILABLE` or
|
|
328
|
+
`CHECKOUT_PROVIDER_UNAVAILABLE` is retried per its `Retry-After` by non-mutating syncs within the same budget, then
|
|
329
|
+
reported; any other 503 (`SITE_KEYS_SYNCING`, `SITE_REGISTRY_WARMING`) and every other non-2xx of the SDK's own calls
|
|
330
|
+
reaches `onProblem(problem, { status })` at once, `status` being the HTTP status — a `409 CHECKOUT_CART_CHANGED`
|
|
331
|
+
carries the corrected cart in `problem.cart`: show it and its warnings, then `handle.sync(() => POST /checkout)`.
|
|
332
|
+
`handle.sync` never rejects. `onProblem` also receives the SDK's own codes: `SDK_SYNC_FAILED` or `SDK_STATUS_FAILED`
|
|
333
|
+
when one of its calls throws (a network failure), `SDK_UPDATE_FAILED` when your `update` throws, and
|
|
334
|
+
`SDK_DISPATCH_FAILED` for any other throw while dispatching (an `onOrder` that throws, an unknown widget controller) —
|
|
335
|
+
each with `{ status: 0 }` and the widget left suspended; `SDK_CHECKOUT_CLOSED` when the one `sync()` after a
|
|
336
|
+
`declined`/`expired` answers closed again, and `SDK_UNKNOWN_STATUS` for an answer status the SDK does not know (both
|
|
337
|
+
`{ status: 200 }`); and `SDK_EMPTY_ANSWER`, which the server helpers return when an answer carried no body (with that
|
|
338
|
+
answer's HTTP status). The SDK also syncs **by itself**: once on the provider's
|
|
339
|
+
payment-declined signal where its JavaScript offers one, when the page becomes visible again while suspended, and
|
|
340
|
+
**three minutes before expiry, timed from the DTO's `expiresInSeconds`** (server-relative, so a browser clock ahead
|
|
341
|
+
of the server's still lands inside the server's five-minute window). An autonomous sync is skipped while another
|
|
342
|
+
sync is running or queued.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// A client component, keyed by attemptId so a re-render never resets it.
|
|
346
|
+
const handle = mountCheckout(container, checkout, {
|
|
347
|
+
sync: callSync, // a Server Action calling syncForBrowser()
|
|
348
|
+
status: callStatus, // a Server Action calling getCheckoutStatus(storefront, attemptId, cartToken)
|
|
349
|
+
onProblem: (problem) => { /* problem.cart when code === "CHECKOUT_CART_CHANGED"; otherwise the message for problem.code */ },
|
|
350
|
+
onOrder: (order, attemptId) => { location.assign(`/kassa/tack?attempt=${attemptId}&order=${order.id}`); },
|
|
351
|
+
});
|
|
352
|
+
// Every cart mutation on the page goes through the handle: suspend → your update once → status first, identity on open.
|
|
353
|
+
await handle.sync(() => callAddLine(sku)); // callAddLine mutates the cart, then syncs, and returns that answer
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**After completion.** Every cart endpoint answers `410 CART_EXPIRED` for the converted cart, while the cart token
|
|
357
|
+
is still what re-delivers the order's access token — so **a `410` from a cart mutation on the checkout or
|
|
358
|
+
confirmation path does not end the cart cookie**: call `GET /v1/checkout/{attemptId}` (or `POST /checkout`, which
|
|
359
|
+
answers the converted cart's completed attempt) with the same token, navigate to the confirmation from its
|
|
360
|
+
`order`, and clear the cookie only once the confirmation page holds the access token.
|
|
361
|
+
|
|
362
|
+
**Copy and CSP.** The `pending` state ("awaiting payment approval") is your copy. Each provider's widget needs its
|
|
363
|
+
domains in your CSP: Qliro One `*.qliro.com` (`pago.qit.nu` in the sandbox), Klarna/Kustom `*.klarna.com`
|
|
364
|
+
(`*.kustom.co`), Walley `*.walleypay.com`, plus their `frame-src`/`script-src`/`connect-src` — the provider's
|
|
365
|
+
integration guide lists the exact hosts.
|
|
366
|
+
|
|
367
|
+
Problem codes you will meet here: `CART_EMPTY`, `CHECKOUT_CART_CHANGED` (with `cart`), `CHECKOUT_PREVIEW_KEY`,
|
|
368
|
+
`CHECKOUT_MARKET_MISMATCH` (switch the cart's market first, then sync), `OPTION_NOT_AVAILABLE`,
|
|
369
|
+
`ORIGIN_NOT_ALLOWED`, `CHECKOUT_UNAVAILABLE` and `CHECKOUT_PROVIDER_UNAVAILABLE` (503, `Retry-After`),
|
|
370
|
+
`CHECKOUT_IN_PROGRESS` (409, `Retry-After`), `CHECKOUT_PROVIDER_REJECTED` (502), `CHECKOUT_NOT_FOUND`,
|
|
371
|
+
`ORDER_NOT_FOUND`, `SITE_KEYS_SYNCING` (503 for a secret key on a freshly created site, `Retry-After`).
|
|
372
|
+
|
|
373
|
+
#### Qliro (`qliro-q1`)
|
|
374
|
+
|
|
375
|
+
- The widget is Qliro's HTML snippet; `mountCheckout` re-creates its scripts so they run.
|
|
376
|
+
- After every cart change, `handle.sync(update)` locks the widget (`q1.lock()`), and resumes it through
|
|
377
|
+
`q1.onOrderUpdated`, which makes the iframe re-read the order. The widget unlocks only when Qliro's order shows the
|
|
378
|
+
confirmed total. If it does not within 10 seconds, or the change came before Qliro's script had loaded, the widget is
|
|
379
|
+
re-rendered from the confirmed checkout instead; it is never unlocked before Qliro shows the confirmed total.
|
|
380
|
+
- When our validation declines a purchase (the cart moved on, the attempt expired, a line is out of stock), Qliro shows
|
|
381
|
+
its out-of-stock message and fires `q1.onPaymentDeclined`; the SDK then syncs by itself and remounts or resumes.
|
|
382
|
+
|
|
196
383
|
## Headers
|
|
197
384
|
|
|
198
385
|
Every call the client makes carries these; you never set them by hand.
|
|
@@ -234,9 +421,14 @@ Codes you will meet: `SITE_KEY_MISSING`, `SITE_KEY_INVALID`, `SITE_INACTIVE`, `S
|
|
|
234
421
|
`SITE_REGISTRY_WARMING`, `MARKET_UNKNOWN`, `RATE_LIMITED` (with `retryAfterSeconds`, plus a `Retry-After`
|
|
235
422
|
header), `PRODUCT_NOT_FOUND`, `COLLECTION_NOT_FOUND`, `INVALID_PAGE`, `INVALID_SINCE`, `INVALID_TYPE`,
|
|
236
423
|
`INDEX_UNAVAILABLE` (retry — a preview index is being rebuilt), `NOT_FOUND`, `INVALID_QUERY`,
|
|
237
|
-
`FACET_NOT_FOUND`, `FACET_NOT_SEARCHABLE
|
|
238
|
-
|
|
239
|
-
`
|
|
424
|
+
`FACET_NOT_FOUND`, `FACET_NOT_SEARCHABLE`,
|
|
425
|
+
and, from the checkout ([above](#checkout)): `CART_EMPTY`, `CHECKOUT_CART_CHANGED`, `CHECKOUT_PREVIEW_KEY`,
|
|
426
|
+
`CHECKOUT_MARKET_MISMATCH`, `OPTION_NOT_AVAILABLE`, `ORIGIN_NOT_ALLOWED`, `CHECKOUT_UNAVAILABLE`,
|
|
427
|
+
`CHECKOUT_PROVIDER_UNAVAILABLE`, `CHECKOUT_IN_PROGRESS`, `CHECKOUT_PROVIDER_REJECTED`, `CHECKOUT_NOT_FOUND`,
|
|
428
|
+
`ORDER_NOT_FOUND`, `SITE_KEYS_SYNCING`, `CART_EXPIRED`.
|
|
429
|
+
|
|
430
|
+
`SITE_REGISTRY_WARMING` and `INDEX_UNAVAILABLE` are retries, not bugs in your call, as are the checkout's
|
|
431
|
+
`CHECKOUT_IN_PROGRESS`, `CHECKOUT_UNAVAILABLE`, `CHECKOUT_PROVIDER_UNAVAILABLE` and `SITE_KEYS_SYNCING` (each with `Retry-After`).
|
|
240
432
|
|
|
241
433
|
## Warnings
|
|
242
434
|
|
|
@@ -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;
|