@wocart/storefront 1.5.0 → 1.7.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 +58 -0
- package/USAGE.md +150 -35
- package/dist/client/core.d.ts +6 -0
- package/dist/client/core.js +24 -4
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +3 -1
- package/dist/client/menus.d.ts +22 -0
- package/dist/client/menus.js +30 -0
- package/dist/client/products.d.ts +19 -7
- package/dist/client/products.js +22 -2
- package/dist/domain.d.ts +6 -0
- package/dist/domain.js +6 -0
- package/dist/endpoints/proxy.d.ts +8 -2
- package/dist/endpoints/proxy.js +5 -2
- package/dist/endpoints/storefront-routes.d.ts +13 -4
- package/dist/endpoints/storefront-routes.js +46 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/review-media.d.ts +87 -0
- package/dist/review-media.js +182 -0
- package/dist/routes.d.ts +60 -28
- package/dist/routes.js +80 -51
- package/dist/schemas.d.ts +91 -3
- package/dist/schemas.js +65 -1
- package/dist/types.d.ts +115 -3
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,64 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
|
|
|
5
5
|
in a new major version, and an export is marked `@deprecated` for at least one
|
|
6
6
|
minor release before a major removes it.
|
|
7
7
|
|
|
8
|
+
## 1.7.0 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Page types and their URLs.** `STOREFRONT_PAGE_PATHS` is the platform's
|
|
13
|
+
table of public paths (`home`, `catalog`, `category`, `product`, `page`,
|
|
14
|
+
`landing`, `checkout`, `thankYou`); `storefrontPath(type, params)` builds
|
|
15
|
+
one (encoded; a missing parameter throws) and `matchStorefrontPath(pathname)`
|
|
16
|
+
recognises one, exactly. Types `StorefrontPageType`, `StorefrontPathParams`,
|
|
17
|
+
`StorefrontPageMatch`.
|
|
18
|
+
- **Navigation menus.** `store.menus.get(handle)` reads a menu as a shopper
|
|
19
|
+
sees it: labels in the client's language, every `href` resolved by the
|
|
20
|
+
server, items leading nowhere public already left out; `null` for an
|
|
21
|
+
unknown handle, which is not reported to `onError`. Types `StoreMenu`,
|
|
22
|
+
`StoreMenuItem`, schemas `StoreMenuSchema`, `StoreMenuItemSchema`.
|
|
23
|
+
- `StoreConfig.themeSettings`: the merchant's values for the requesting
|
|
24
|
+
theme's settings, as saved — resolve them with `resolveThemeSettings` from
|
|
25
|
+
`@wocart/theme` 1.1. Optional: absent from a server that predates them.
|
|
26
|
+
|
|
27
|
+
### Deprecated
|
|
28
|
+
|
|
29
|
+
- `STOREFRONT_ROUTES` lists six of the paths, without the home and the
|
|
30
|
+
catalog; use `STOREFRONT_PAGE_PATHS` and `matchStorefrontPath`.
|
|
31
|
+
|
|
32
|
+
## 1.6.0 — 2026-10-17
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **Reviews with photos and a voice note.** `products.reviews.eligibility()`
|
|
37
|
+
proves a purchase with the order number and the phone it was placed with,
|
|
38
|
+
and answers the products the order may review and what may be attached,
|
|
39
|
+
with an upload grant. `products.reviews.upload()` sends one file as it is
|
|
40
|
+
(a stream passes through unbuffered). `submit()` takes `phone`, `grant` and
|
|
41
|
+
`uploads`, and answers `visibility` (`published` or `after_review`).
|
|
42
|
+
- `Review` carries `verified` and `media` (photos with a thumbnail, the voice
|
|
43
|
+
note with its length); reviews from older servers read as unverified with
|
|
44
|
+
no media.
|
|
45
|
+
- Storefront routes `reviewEligibility` and `reviewUpload` (PUT, the file as
|
|
46
|
+
the body); `reviewSubmit` forwards the new fields. Every PUT refuses a
|
|
47
|
+
cross-origin request too.
|
|
48
|
+
- **`@wocart/storefront/review-media`**, a browser-only entry point:
|
|
49
|
+
`prepareReviewPhoto` (1600 px, re-encoded, EXIF removed before it leaves
|
|
50
|
+
the device), `createVoiceRecorder` (speech at ~32 kb/s, stops at the
|
|
51
|
+
store's limit), `canRecordVoice`, `pickVoiceMimeType`, `photoTargetSize`.
|
|
52
|
+
- Types `ReviewMedia`, `ReviewEligibility`, `ReviewEligibilityInput`,
|
|
53
|
+
`ReviewableProduct`, `ReviewUpload`, `ReviewUploadInput`,
|
|
54
|
+
`ReviewSubmitResult`, and their schemas.
|
|
55
|
+
|
|
56
|
+
### Deprecated
|
|
57
|
+
|
|
58
|
+
- **`submit()` without `phone`**: still accepted for a text review of a
|
|
59
|
+
delivered order; a later major requires the phone.
|
|
60
|
+
|
|
61
|
+
- **`deletedAt`** on a product is optional and always `null` when present: a
|
|
62
|
+
removed product is deleted on the server and never served, so the field says
|
|
63
|
+
nothing. The server keeps sending `null` for releases that require it; a
|
|
64
|
+
later major release removes it. Nothing in a theme should read it.
|
|
65
|
+
|
|
8
66
|
## 1.5.0 — 2026-10-15
|
|
9
67
|
|
|
10
68
|
### Added
|
package/USAGE.md
CHANGED
|
@@ -90,34 +90,55 @@ allowlists, delivery availability, stock, discounts, final totals, OTP
|
|
|
90
90
|
acceptance, or tracking eligibility. Those rules are platform data or platform
|
|
91
91
|
decisions. The theme presents them and gives the shopper a clear next action.
|
|
92
92
|
|
|
93
|
-
##
|
|
93
|
+
## Page types and their URLs
|
|
94
94
|
|
|
95
|
-
A
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
A URL *means* something to the platform — a product, a category, a store
|
|
96
|
+
page — and that meaning is a **page type**. Each page type lives at one
|
|
97
|
+
public path. The paths are a platform contract, not a theme preference: the
|
|
98
|
+
platform builds them without running your theme (the dashboard prints a page's
|
|
99
|
+
public link, the server names the landing page in the URL Meta is told about,
|
|
100
|
+
the AI tools hand out landing-page links), so a theme that moved one would
|
|
101
|
+
break those links.
|
|
101
102
|
|
|
102
|
-
|
|
|
103
|
-
| --- | --- |
|
|
104
|
-
|
|
|
105
|
-
| `/
|
|
106
|
-
| `/
|
|
107
|
-
| `/
|
|
108
|
-
| `/
|
|
109
|
-
| `/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
| Page type | Path | Renders | Your theme |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `home` | `/` | the home page | required |
|
|
106
|
+
| `catalog` | `/products` | every product | required |
|
|
107
|
+
| `category` | `/category/:slug` | one category's products | required when the store has categories |
|
|
108
|
+
| `product` | `/products/:handle` | one product (its public handle, never an id) | required |
|
|
109
|
+
| `page` | `/pages/:slug` | a legal or merchant page | required — checkout consent links to the store's legal pages |
|
|
110
|
+
| `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
|
|
111
|
+
| `checkout` | `/checkout` | the cart's checkout | required when the store turns the cart on |
|
|
112
|
+
| `thankYou` | `/thank-you` | the order confirmation | required |
|
|
113
|
+
|
|
114
|
+
How a theme implements them, in Astro's own terms:
|
|
115
|
+
|
|
116
|
+
- **Render each page type at its path with a file route**:
|
|
117
|
+
`src/pages/products/[handle].astro`, `src/pages/lp/[slug].astro` … The
|
|
118
|
+
parameter's name in your file is yours; the path is the platform's.
|
|
119
|
+
- **Build every link with `storefrontPath`**, never a hand-written string:
|
|
120
|
+
`storefrontPath("product", { handle: product.handle })` → `/products/robe`,
|
|
121
|
+
`storefrontPath("checkout")` → `/checkout`. Parameters are encoded for you;
|
|
122
|
+
a missing one throws, so a link to nowhere fails loudly. Add the language
|
|
123
|
+
prefix with `localizeHref(storefrontPath(…), locale, routing)`.
|
|
124
|
+
- **Recognise a page with `matchStorefrontPath(pathname)`** →
|
|
125
|
+
`{ type: "product", params: { handle: "robe" } }`, or `null` for any other
|
|
126
|
+
path. It matches exactly — `/productsomething`, `/products/a/b` and
|
|
127
|
+
`/checkout-success` are nothing — and ignores the query, fragment and a
|
|
128
|
+
trailing slash. Pass the path without its language prefix.
|
|
129
|
+
- **Answer a missing resource with a real 404** (`return Astro.rewrite("/404")`),
|
|
130
|
+
never a redirect to the home page: a redirect turns a dead link into a
|
|
131
|
+
"soft 404" for search engines and ad reviews.
|
|
132
|
+
- **Every other route is yours.** `/about`, `/offers/summer`, a store
|
|
133
|
+
locator — add them as Astro file routes; nothing in the contract restricts
|
|
134
|
+
them. Avoid the platform's paths above and `/api/*` (your same-origin
|
|
135
|
+
endpoints, see "Storefront API routes").
|
|
136
|
+
|
|
137
|
+
`showsWidgetOn(surfaces, pathname)` is the platform's half of "should the
|
|
138
|
+
WhatsApp widget show here": the merchant switches surfaces on; the page type
|
|
139
|
+
says which surface a pathname is. `STOREFRONT_PAGE_PATHS` is the table above
|
|
140
|
+
as data. `STOREFRONT_ROUTES` is deprecated: it lists six of the paths, without
|
|
141
|
+
the home and the catalog.
|
|
121
142
|
|
|
122
143
|
## The five rules to remember
|
|
123
144
|
|
|
@@ -196,7 +217,8 @@ server directly.
|
|
|
196
217
|
| `WILAYAS`, `ALGERIA_COMMUNES`, `communesForWilaya` | Reference data: 58 wilayas, 1,551 communes, one wilaya's rows |
|
|
197
218
|
| `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
|
|
198
219
|
| `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | WhatsApp prefill text and the `wa.me` link |
|
|
199
|
-
| `
|
|
220
|
+
| `storefrontPath`, `matchStorefrontPath`, `STOREFRONT_PAGE_PATHS` | The page types and their public paths: build a link, recognise a page |
|
|
221
|
+
| `showsWidgetOn`, `STOREFRONT_ROUTES` | Which widget surface a pathname is (`STOREFRONT_ROUTES` deprecated) |
|
|
200
222
|
| `productImageSource` | Which photo represents a product (cover → gallery fallback) |
|
|
201
223
|
| `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
|
|
202
224
|
| `getOfferTierView` | Pure offer-tier price, compare-price, savings, and free-shipping projection; display-only, never order authority |
|
|
@@ -295,6 +317,7 @@ carries the key, which the package's own contract test asserts.
|
|
|
295
317
|
| products | Product catalog and reviews | Product, ProductList, or empty/null |
|
|
296
318
|
| categories | Catalog navigation | Category array |
|
|
297
319
|
| pages | Legal and CMS pages, landing pages | StorePagePublic, LandingPage, or null |
|
|
320
|
+
| menus | The store's navigation menus | StoreMenu or null |
|
|
298
321
|
| pageData | The store's saved page document | PageDocumentPayload or null |
|
|
299
322
|
| shipping | Delivery prices | ShippingRates or one rate/null |
|
|
300
323
|
| geo | Wilayas and communes | Static wilayas or Commune array |
|
|
@@ -474,8 +497,8 @@ StoreConfig contains the current store identity and presentation fields,
|
|
|
474
497
|
including:
|
|
475
498
|
|
|
476
499
|
- name, domain, logoUrl, themeId, colors, fontFamily, and fontUrl;
|
|
477
|
-
- lang (the primary language), locales, contentOverrides,
|
|
478
|
-
currencySymbol, metaTitle, metaDescription, and ogImage;
|
|
500
|
+
- lang (the primary language), locales, contentOverrides, themeSettings,
|
|
501
|
+
currency, currencySymbol, metaTitle, metaDescription, and ogImage;
|
|
479
502
|
- announcementBar and reviewsEnabled;
|
|
480
503
|
- cartEnabled, freeShippingThreshold, and cartShippingMode;
|
|
481
504
|
- checkoutForm policy;
|
|
@@ -483,6 +506,21 @@ including:
|
|
|
483
506
|
- pixelId and conversionEvent;
|
|
484
507
|
- public page links, legal contact information, and WhatsApp widget settings.
|
|
485
508
|
|
|
509
|
+
`themeSettings` holds the merchant's values for your theme's settings (the
|
|
510
|
+
ones it declares with `defineTheme({ settings })` in `@wocart/theme`), by
|
|
511
|
+
setting key, exactly as saved. It is sent only to a client created with
|
|
512
|
+
`theme`, is `{}` when the store saved none, and is absent from a server that
|
|
513
|
+
predates theme settings — `resolveThemeSettings` takes either. Never read it raw: resolve it
|
|
514
|
+
against your declaration, which checks every value and falls back to your
|
|
515
|
+
default for one it no longer accepts:
|
|
516
|
+
|
|
517
|
+
~~~ts
|
|
518
|
+
import { resolveThemeSettings } from "@wocart/theme";
|
|
519
|
+
|
|
520
|
+
const { values } = resolveThemeSettings(theme, config?.themeSettings, { locales: [locale] });
|
|
521
|
+
const headerMenu = values.headerMenu; // a menu handle, for store.menus.get()
|
|
522
|
+
~~~
|
|
523
|
+
|
|
486
524
|
The checkout policy is not a suggestion. Use it to decide whether address,
|
|
487
525
|
email, notes, delivery options, and custom fields are required, optional, or
|
|
488
526
|
hidden. The policy includes:
|
|
@@ -624,19 +662,64 @@ const reviews = await store.products.reviews.list(productId, {
|
|
|
624
662
|
~~~
|
|
625
663
|
|
|
626
664
|
The result is { rows: Review[], total: number }. The default page size is 20
|
|
627
|
-
and the default offset is 0.
|
|
665
|
+
and the default offset is 0. Each review carries `verified` (it rests on an
|
|
666
|
+
order that reached its customer — show a "verified purchase" badge) and
|
|
667
|
+
`media`: its photos (`url` at 1600 px, `thumbUrl` at 480 px for lists, with
|
|
668
|
+
`width`/`height` to reserve the space) and its voice note (`kind: "audio"`,
|
|
669
|
+
with `durationMs`). Show photos as their `thumbUrl` with `loading="lazy"`, and
|
|
670
|
+
a voice note as `<audio preload="none">`, so neither costs anything until the
|
|
671
|
+
shopper opens or plays it.
|
|
672
|
+
|
|
673
|
+
Writing a review takes three steps, so the shopper never waits on a slow
|
|
674
|
+
connection:
|
|
628
675
|
|
|
629
676
|
~~~ts
|
|
677
|
+
// 1. Prove the purchase: the order number and the phone it was placed with.
|
|
678
|
+
const found = await store.products.reviews.eligibility({ orderNumber, phone });
|
|
679
|
+
if (!found.success) {
|
|
680
|
+
// ORDER_NOT_FOUND (wrong number or phone), REVIEW_ORDER_NOT_DELIVERED,
|
|
681
|
+
// REVIEW_RATE_LIMITED, REVIEWS_DISABLED
|
|
682
|
+
}
|
|
683
|
+
// found.data.products: what the order may review ({ reviewed } ones only shown).
|
|
684
|
+
// found.data.media: null (text only) or { maxPhotos, voiceMaxSeconds, grant }.
|
|
685
|
+
|
|
686
|
+
// 2. Send each file the moment it is picked — not on submit.
|
|
687
|
+
const sent = await store.products.reviews.upload({ grant, body: file, contentLength: file.size });
|
|
688
|
+
// sent.data.uploadId
|
|
689
|
+
|
|
690
|
+
// 3. Submit: answered at once; the files are prepared afterwards.
|
|
630
691
|
const submitted = await store.products.reviews.submit({
|
|
631
|
-
orderNumber
|
|
692
|
+
orderNumber,
|
|
693
|
+
phone,
|
|
632
694
|
productId,
|
|
633
695
|
rating: 5,
|
|
634
|
-
title: "Beautiful",
|
|
635
696
|
body: "Exactly as described.",
|
|
697
|
+
grant,
|
|
698
|
+
uploads: [uploadId],
|
|
636
699
|
});
|
|
700
|
+
// submitted.data.visibility: "published" now, or "after_review"
|
|
701
|
+
~~~
|
|
702
|
+
|
|
703
|
+
orderNumber is the customer-visible order number, not the internal order
|
|
704
|
+
UUID. The body needs 10 characters unless a voice note is attached. One review
|
|
705
|
+
per product of an order (`ORDER_ALREADY_REVIEWED`). A submit without `phone`
|
|
706
|
+
— the 1.5 shape — still works for text only, on a delivered order, and is
|
|
707
|
+
deprecated.
|
|
708
|
+
|
|
709
|
+
In the browser, `@wocart/storefront/review-media` prepares files before they
|
|
710
|
+
are sent:
|
|
711
|
+
|
|
712
|
+
~~~ts
|
|
713
|
+
import { canRecordVoice, createVoiceRecorder, prepareReviewPhoto } from "@wocart/storefront/review-media";
|
|
714
|
+
|
|
715
|
+
const photo = await prepareReviewPhoto(file); // 1600 px, WebP/JPEG, EXIF gone
|
|
716
|
+
const recorder = createVoiceRecorder({ maxSeconds: media.voiceMaxSeconds, onTick, onLimit });
|
|
717
|
+
await recorder.start();
|
|
718
|
+
const { blob, durationMs } = await recorder.stop();
|
|
637
719
|
~~~
|
|
638
720
|
|
|
639
|
-
|
|
721
|
+
`canRecordVoice()` says whether to show a record button at all; a shopper can
|
|
722
|
+
also attach an audio file. Send a recording with its `durationMs`.
|
|
640
723
|
|
|
641
724
|
## Categories, legal pages, and landing pages
|
|
642
725
|
|
|
@@ -671,6 +754,36 @@ Do not hardcode the legal navigation list. Use config.pages so the store's
|
|
|
671
754
|
available public pages determine the links. If a page is null, render the
|
|
672
755
|
theme's not-found or fallback state.
|
|
673
756
|
|
|
757
|
+
### Navigation menus
|
|
758
|
+
|
|
759
|
+
A store's menus are store data, like its products: the merchant builds them in
|
|
760
|
+
the dashboard (Navigation) — a handle such as `main-menu`, a title, and up to
|
|
761
|
+
three levels of items, each pointing at a product, a category, a store page, a
|
|
762
|
+
landing page, the home, the catalog, or a URL. Your theme decides *where* a
|
|
763
|
+
menu shows; the merchant decides *what* it holds.
|
|
764
|
+
|
|
765
|
+
~~~ts
|
|
766
|
+
const menu = await store.menus.get(settings.headerMenu); // "main-menu"
|
|
767
|
+
|
|
768
|
+
for (const item of menu?.items ?? []) {
|
|
769
|
+
const href = item.type === "url" ? item.href : localizeHref(item.href, locale, routing);
|
|
770
|
+
// item.label, item.children (same shape, at most three levels)
|
|
771
|
+
}
|
|
772
|
+
~~~
|
|
773
|
+
|
|
774
|
+
- Name the menu through a `menu` theme setting rather than a hard-coded
|
|
775
|
+
handle, so the merchant picks which menu each place shows.
|
|
776
|
+
- Every link is resolved by the server: `href` is a path of the store without
|
|
777
|
+
a language prefix (pass it through `localizeHref`) or, for `type: "url"`,
|
|
778
|
+
the address the merchant entered — render it as is.
|
|
779
|
+
- An item whose destination is deleted or not public (a draft page, an
|
|
780
|
+
inactive product) is already left out, with its children. You never filter.
|
|
781
|
+
- Labels arrive in the client's language when the store translated them.
|
|
782
|
+
- `null` means the store has no menu with that handle, or the read failed:
|
|
783
|
+
render your own fallback navigation (for example the home and the catalog
|
|
784
|
+
from `storefrontPath`). A missing menu is not a failure and is not reported
|
|
785
|
+
to `onError`.
|
|
786
|
+
|
|
674
787
|
### Landing pages
|
|
675
788
|
|
|
676
789
|
~~~ts
|
|
@@ -1179,11 +1292,13 @@ export const POST = routes.otpSend;
|
|
|
1179
1292
|
| `abandonedConvert` | POST | `{ sessionId, orderId, orderNumber }` | 204 |
|
|
1180
1293
|
| `communes` | GET | path param `wilayaId` | `{ data: Commune[] }` |
|
|
1181
1294
|
| `reviewsList` | GET | path param `productId`, query `limit` (1–50, default 20) and `offset` | `{ data: Review[], total }` |
|
|
1182
|
-
| `
|
|
1295
|
+
| `reviewEligibility` | POST | `{ orderNumber, phone, turnstileToken? }` | `{ data: ReviewEligibility }` |
|
|
1296
|
+
| `reviewUpload` | PUT | the file as the body; headers `X-Review-Grant`, `X-Media-Duration-Ms` | 201 `{ data: ReviewUpload }` (streamed on, never buffered) |
|
|
1297
|
+
| `reviewSubmit` | POST | `ReviewInput` | 201 `{ success: true, data: { id, visibility } }` |
|
|
1183
1298
|
|
|
1184
1299
|
What every route gets right:
|
|
1185
1300
|
|
|
1186
|
-
- every POST refuses a cross-origin request with 403;
|
|
1301
|
+
- every POST and PUT refuses a cross-origin request with 403;
|
|
1187
1302
|
- the OTP routes and abandoned tracking forward the shopper's address and
|
|
1188
1303
|
user agent, so the server limits and attributes per shopper;
|
|
1189
1304
|
- input is bounded before it is sent, and a malformed body is a 400 that never
|
package/dist/client/core.d.ts
CHANGED
|
@@ -55,10 +55,16 @@ export interface PatchOptions<TOutput> extends RequestOptions {
|
|
|
55
55
|
* Client core infrastructure
|
|
56
56
|
* Exposed only to namespace implementations, not to end users
|
|
57
57
|
*/
|
|
58
|
+
/** What a file upload's body may be. */
|
|
59
|
+
export type UploadBody = Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
|
|
58
60
|
export interface ClientCore {
|
|
59
61
|
config: ClientConfig;
|
|
60
62
|
get: <T>(path: string, schema?: z.Schema<T>, opts?: GetOptions) => Promise<T>;
|
|
61
63
|
post: <TInput, TOutput>(path: string, body: TInput, opts?: PostOptions<TOutput>) => Promise<ClientResult<TOutput>>;
|
|
64
|
+
/** PUT one file as the body (streams pass through unbuffered). */
|
|
65
|
+
upload: <TOutput>(path: string, body: UploadBody, opts?: PostOptions<TOutput> & {
|
|
66
|
+
contentLength?: number;
|
|
67
|
+
}) => Promise<ClientResult<TOutput>>;
|
|
62
68
|
patch: <TInput, TOutput>(path: string, body: TInput, optsOrSchema?: z.Schema<TOutput> | PatchOptions<TOutput>) => Promise<ClientResult<TOutput>>;
|
|
63
69
|
}
|
|
64
70
|
/** How long a read may take, retries included, unless the client sets another limit. */
|
package/dist/client/core.js
CHANGED
|
@@ -411,8 +411,22 @@ export function createClientCore(config) {
|
|
|
411
411
|
* outcome `unknown`, never a retry of a possible duplicate.
|
|
412
412
|
*/
|
|
413
413
|
async function post(path, body, opts) {
|
|
414
|
+
return write("POST", path, { body: JSON.stringify(body) }, opts);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* PUT one file as the request body, as it is — a stream passes through
|
|
418
|
+
* without being buffered. Same result contract as `post`.
|
|
419
|
+
*/
|
|
420
|
+
async function upload(path, body, opts) {
|
|
421
|
+
return write("PUT", path, {
|
|
422
|
+
body,
|
|
423
|
+
contentType: "application/octet-stream",
|
|
424
|
+
...(opts?.contentLength !== undefined ? { contentLength: opts.contentLength } : {}),
|
|
425
|
+
}, opts);
|
|
426
|
+
}
|
|
427
|
+
async function write(method, path, outgoing, opts) {
|
|
414
428
|
const url = buildUrl(path);
|
|
415
|
-
const operation = opts?.operation ??
|
|
429
|
+
const operation = opts?.operation ?? `${method} ${path}`;
|
|
416
430
|
const request = composeSignal(opts?.signal, opts?.timeoutMs);
|
|
417
431
|
if (request.aborted()) {
|
|
418
432
|
request.cleanup();
|
|
@@ -421,9 +435,14 @@ export function createClientCore(config) {
|
|
|
421
435
|
}
|
|
422
436
|
try {
|
|
423
437
|
const response = await fetchWithRetry(url, {
|
|
424
|
-
method
|
|
425
|
-
headers: buildHeaders(
|
|
426
|
-
|
|
438
|
+
method,
|
|
439
|
+
headers: buildHeaders({
|
|
440
|
+
...(outgoing.contentType ? { "Content-Type": outgoing.contentType } : {}),
|
|
441
|
+
...(outgoing.contentLength !== undefined ? { "Content-Length": String(outgoing.contentLength) } : {}),
|
|
442
|
+
...opts?.headers,
|
|
443
|
+
}),
|
|
444
|
+
body: outgoing.body,
|
|
445
|
+
...(outgoing.body instanceof ReadableStream ? { duplex: "half" } : {}),
|
|
427
446
|
...(request.signal ? { signal: request.signal } : {}),
|
|
428
447
|
}, 0, request.signal);
|
|
429
448
|
const read = await readBody(response);
|
|
@@ -523,6 +542,7 @@ export function createClientCore(config) {
|
|
|
523
542
|
config,
|
|
524
543
|
get,
|
|
525
544
|
post,
|
|
545
|
+
upload,
|
|
526
546
|
patch,
|
|
527
547
|
};
|
|
528
548
|
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Store Client Factory — the SDK's front door.
|
|
3
3
|
*
|
|
4
4
|
* One server-side call builds the whole read/write surface a theme needs, over one
|
|
5
|
-
* shared core: config, products, categories, pages, shipping, geo, cart,
|
|
5
|
+
* shared core: config, products, categories, pages, menus, shipping, geo, cart,
|
|
6
6
|
* orders. The core is internal — consumers see only namespaces.
|
|
7
7
|
*
|
|
8
8
|
* One core means one connection story for every namespace: every read is
|
|
@@ -31,6 +31,7 @@ export declare function createStoreClient(config: StoreScope): {
|
|
|
31
31
|
products: import("./products.js").ProductsNamespace;
|
|
32
32
|
categories: import("./categories.js").CategoriesNamespace;
|
|
33
33
|
pages: import("./pages.js").PagesNamespace;
|
|
34
|
+
menus: import("./menus.js").MenusNamespace;
|
|
34
35
|
pageData: import("./page-data.js").PageDataNamespace;
|
|
35
36
|
shipping: import("./shipping.js").ShippingNamespace;
|
|
36
37
|
geo: import("./geo.js").GeoNamespace;
|
package/dist/client/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Store Client Factory — the SDK's front door.
|
|
3
3
|
*
|
|
4
4
|
* One server-side call builds the whole read/write surface a theme needs, over one
|
|
5
|
-
* shared core: config, products, categories, pages, shipping, geo, cart,
|
|
5
|
+
* shared core: config, products, categories, pages, menus, shipping, geo, cart,
|
|
6
6
|
* orders. The core is internal — consumers see only namespaces.
|
|
7
7
|
*
|
|
8
8
|
* One core means one connection story for every namespace: every read is
|
|
@@ -31,6 +31,7 @@ import { createConfigNamespace } from "./config.js";
|
|
|
31
31
|
import { createProductsNamespace } from "./products.js";
|
|
32
32
|
import { createCategoriesNamespace } from "./categories.js";
|
|
33
33
|
import { createPagesNamespace } from "./pages.js";
|
|
34
|
+
import { createMenusNamespace } from "./menus.js";
|
|
34
35
|
import { createPageDataNamespace } from "./page-data.js";
|
|
35
36
|
import { createShippingNamespace } from "./shipping.js";
|
|
36
37
|
import { createGeoNamespace } from "./geo.js";
|
|
@@ -44,6 +45,7 @@ export function createStoreClient(config) {
|
|
|
44
45
|
products: createProductsNamespace(core),
|
|
45
46
|
categories: createCategoriesNamespace(core),
|
|
46
47
|
pages: createPagesNamespace(core),
|
|
48
|
+
menus: createMenusNamespace(core),
|
|
47
49
|
pageData: createPageDataNamespace(core),
|
|
48
50
|
shipping: createShippingNamespace(core),
|
|
49
51
|
geo: createGeoNamespace(core),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menus Namespace - the store's navigation menus
|
|
3
|
+
*
|
|
4
|
+
* A menu is store data the merchant edits in the dashboard; a theme names it
|
|
5
|
+
* by handle (usually through a `menu` theme setting) and renders what comes
|
|
6
|
+
* back. Every link is resolved by the server: an item's `href` is a path of
|
|
7
|
+
* the store without a language prefix, or the merchant's URL, and items
|
|
8
|
+
* leading nowhere public are already left out. Fetched fresh every request.
|
|
9
|
+
*
|
|
10
|
+
* Failure contract: `get(handle)` → null means "no such menu" or a failed
|
|
11
|
+
* read — the theme renders its own fallback navigation.
|
|
12
|
+
*/
|
|
13
|
+
import { type ClientCore } from "./core.js";
|
|
14
|
+
import type { StoreMenu } from "../types.js";
|
|
15
|
+
export interface MenusNamespace {
|
|
16
|
+
/**
|
|
17
|
+
* A menu by handle (`main-menu`), labels in the client's language.
|
|
18
|
+
* Resolves to `null` for an unknown handle or a failed read.
|
|
19
|
+
*/
|
|
20
|
+
get(handle: string): Promise<StoreMenu | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createMenusNamespace(core: ClientCore): MenusNamespace;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menus Namespace - the store's navigation menus
|
|
3
|
+
*
|
|
4
|
+
* A menu is store data the merchant edits in the dashboard; a theme names it
|
|
5
|
+
* by handle (usually through a `menu` theme setting) and renders what comes
|
|
6
|
+
* back. Every link is resolved by the server: an item's `href` is a path of
|
|
7
|
+
* the store without a language prefix, or the merchant's URL, and items
|
|
8
|
+
* leading nowhere public are already left out. Fetched fresh every request.
|
|
9
|
+
*
|
|
10
|
+
* Failure contract: `get(handle)` → null means "no such menu" or a failed
|
|
11
|
+
* read — the theme renders its own fallback navigation.
|
|
12
|
+
*/
|
|
13
|
+
import { StoreMenuSchema } from "../schemas.js";
|
|
14
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
15
|
+
import { localizedPath } from "./paths.js";
|
|
16
|
+
import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
|
|
17
|
+
export function createMenusNamespace(core) {
|
|
18
|
+
return {
|
|
19
|
+
async get(handle) {
|
|
20
|
+
try {
|
|
21
|
+
return await core.get(localizedPath(core.config, `/store/menus/${encodeURIComponent(handle)}`), StoreMenuSchema, { operation: STOREFRONT_OPERATIONS.menusGet });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!isNotFoundAnswer(error))
|
|
25
|
+
reportDegradedRead(core.config, error, "menus.get");
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* half-shaped product into a theme.
|
|
13
13
|
*/
|
|
14
14
|
import { type ClientCore, type ClientResult } from "./core.js";
|
|
15
|
-
import type { Product, ProductList, ProductPage, ProductListFilters, Review, ReviewInput, ReviewListPagination } from "../types.js";
|
|
15
|
+
import type { Product, ProductList, ProductPage, ProductListFilters, Review, ReviewEligibility, ReviewEligibilityInput, ReviewInput, ReviewListPagination, ReviewSubmitResult, ReviewUpload, ReviewUploadInput } from "../types.js";
|
|
16
16
|
/** Page size when a caller names none — the storefront's default grid. */
|
|
17
17
|
export declare const PRODUCT_PAGE_DEFAULT = 24;
|
|
18
18
|
/** One below the server's 100-row cap, so the extra row that proves a next page fits. */
|
|
@@ -51,13 +51,25 @@ export interface ProductsNamespace {
|
|
|
51
51
|
total: number;
|
|
52
52
|
}>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* Prove a purchase with the order number and the phone it was placed
|
|
55
|
+
* with: answers the order's products the shopper may review (a delivered
|
|
56
|
+
* order, each product once) and, when the store takes them, what may be
|
|
57
|
+
* attached and the upload grant. A wrong number or phone fails with
|
|
58
|
+
* `ORDER_NOT_FOUND`; an order on its way with `REVIEW_ORDER_NOT_DELIVERED`.
|
|
57
59
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
eligibility(input: ReviewEligibilityInput): Promise<ClientResult<ReviewEligibility>>;
|
|
61
|
+
/**
|
|
62
|
+
* Send one photo or voice note, as it is, under the grant — as soon as the
|
|
63
|
+
* shopper picks it, so submitting waits for nothing. A stream passes
|
|
64
|
+
* through unbuffered. Its kind is read from its bytes by the server.
|
|
65
|
+
*/
|
|
66
|
+
upload(input: ReviewUploadInput): Promise<ClientResult<ReviewUpload>>;
|
|
67
|
+
/**
|
|
68
|
+
* Submit a review. Answers at once; its files are prepared afterwards,
|
|
69
|
+
* and `visibility` says whether it is public now or after the store
|
|
70
|
+
* checks it.
|
|
71
|
+
*/
|
|
72
|
+
submit(data: ReviewInput): Promise<ClientResult<ReviewSubmitResult>>;
|
|
61
73
|
};
|
|
62
74
|
}
|
|
63
75
|
/**
|
package/dist/client/products.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* half-shaped product into a theme.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from "zod";
|
|
15
|
-
import {
|
|
15
|
+
import { ProductListSchema, ProductSchema, ReviewEligibilitySchema, ReviewListEnvelopeSchema, ReviewSubmitResultSchema, ReviewUploadSchema } from "../schemas.js";
|
|
16
16
|
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
17
17
|
import { localizedPath } from "./paths.js";
|
|
18
18
|
import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
|
|
@@ -81,8 +81,28 @@ export function createProductsNamespace(core) {
|
|
|
81
81
|
return { rows: [], total: 0 };
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
|
+
async eligibility(input) {
|
|
85
|
+
return core.post("/store/reviews/eligibility", input, {
|
|
86
|
+
schema: ReviewEligibilitySchema,
|
|
87
|
+
operation: STOREFRONT_OPERATIONS.productsReviewsEligibility,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async upload(input) {
|
|
91
|
+
return core.upload("/store/reviews/uploads", input.body, {
|
|
92
|
+
schema: ReviewUploadSchema,
|
|
93
|
+
operation: STOREFRONT_OPERATIONS.productsReviewsUpload,
|
|
94
|
+
headers: {
|
|
95
|
+
"X-Review-Grant": input.grant,
|
|
96
|
+
...(input.durationMs ? { "X-Media-Duration-Ms": String(Math.round(input.durationMs)) } : {}),
|
|
97
|
+
},
|
|
98
|
+
...(input.contentLength !== undefined ? { contentLength: input.contentLength } : {}),
|
|
99
|
+
});
|
|
100
|
+
},
|
|
84
101
|
async submit(data) {
|
|
85
|
-
return core.post("/store/reviews", data, {
|
|
102
|
+
return core.post("/store/reviews", data, {
|
|
103
|
+
schema: ReviewSubmitResultSchema,
|
|
104
|
+
operation: STOREFRONT_OPERATIONS.productsReviewsSubmit,
|
|
105
|
+
});
|
|
86
106
|
},
|
|
87
107
|
},
|
|
88
108
|
};
|
package/dist/domain.d.ts
CHANGED
|
@@ -349,8 +349,11 @@ export declare const STOREFRONT_OPERATIONS: {
|
|
|
349
349
|
readonly productsList: "products.list";
|
|
350
350
|
readonly productsReviewsList: "products.reviews.list";
|
|
351
351
|
readonly productsReviewsSubmit: "products.reviews.submit";
|
|
352
|
+
readonly productsReviewsEligibility: "products.reviews.eligibility";
|
|
353
|
+
readonly productsReviewsUpload: "products.reviews.upload";
|
|
352
354
|
readonly pagesGet: "pages.get";
|
|
353
355
|
readonly pagesLanding: "pages.landing";
|
|
356
|
+
readonly menusGet: "menus.get";
|
|
354
357
|
readonly shippingRates: "shipping.rates";
|
|
355
358
|
readonly shippingRate: "shipping.rate";
|
|
356
359
|
readonly geoCommunes: "geo.communes";
|
|
@@ -388,10 +391,13 @@ export declare const STOREFRONT_RETRY_POLICIES: {
|
|
|
388
391
|
readonly "pageData.get": "safe-read";
|
|
389
392
|
readonly "pages.get": "safe-read";
|
|
390
393
|
readonly "pages.landing": "safe-read";
|
|
394
|
+
readonly "menus.get": "safe-read";
|
|
391
395
|
readonly "products.get": "safe-read";
|
|
392
396
|
readonly "products.list": "safe-read";
|
|
393
397
|
readonly "products.reviews.list": "safe-read";
|
|
394
398
|
readonly "products.reviews.submit": "never";
|
|
399
|
+
readonly "products.reviews.eligibility": "never";
|
|
400
|
+
readonly "products.reviews.upload": "never";
|
|
395
401
|
readonly "shipping.rate": "safe-read";
|
|
396
402
|
readonly "shipping.rates": "safe-read";
|
|
397
403
|
};
|
package/dist/domain.js
CHANGED
|
@@ -202,8 +202,11 @@ export const STOREFRONT_OPERATIONS = {
|
|
|
202
202
|
productsList: "products.list",
|
|
203
203
|
productsReviewsList: "products.reviews.list",
|
|
204
204
|
productsReviewsSubmit: "products.reviews.submit",
|
|
205
|
+
productsReviewsEligibility: "products.reviews.eligibility",
|
|
206
|
+
productsReviewsUpload: "products.reviews.upload",
|
|
205
207
|
pagesGet: "pages.get",
|
|
206
208
|
pagesLanding: "pages.landing",
|
|
209
|
+
menusGet: "menus.get",
|
|
207
210
|
shippingRates: "shipping.rates",
|
|
208
211
|
shippingRate: "shipping.rate",
|
|
209
212
|
geoCommunes: "geo.communes",
|
|
@@ -240,10 +243,13 @@ export const STOREFRONT_RETRY_POLICIES = {
|
|
|
240
243
|
[STOREFRONT_OPERATIONS.pageDataGet]: "safe-read",
|
|
241
244
|
[STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
|
|
242
245
|
[STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
|
|
246
|
+
[STOREFRONT_OPERATIONS.menusGet]: "safe-read",
|
|
243
247
|
[STOREFRONT_OPERATIONS.productsGet]: "safe-read",
|
|
244
248
|
[STOREFRONT_OPERATIONS.productsList]: "safe-read",
|
|
245
249
|
[STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
|
|
246
250
|
[STOREFRONT_OPERATIONS.productsReviewsSubmit]: "never",
|
|
251
|
+
[STOREFRONT_OPERATIONS.productsReviewsEligibility]: "never",
|
|
252
|
+
[STOREFRONT_OPERATIONS.productsReviewsUpload]: "never",
|
|
247
253
|
[STOREFRONT_OPERATIONS.shippingRate]: "safe-read",
|
|
248
254
|
[STOREFRONT_OPERATIONS.shippingRates]: "safe-read",
|
|
249
255
|
};
|
|
@@ -38,8 +38,14 @@ export type ProxyClientFn<TInput, TOutput> = (data: TInput, headers?: Record<str
|
|
|
38
38
|
* Configuration for creating a proxy endpoint
|
|
39
39
|
*/
|
|
40
40
|
export interface ProxyEndpointConfig<TInput, TOutput> {
|
|
41
|
-
/** HTTP method: 'GET' or '
|
|
42
|
-
method: "GET" | "POST";
|
|
41
|
+
/** HTTP method: 'GET', 'POST', or 'PUT' (a raw body, read by `readInput`). */
|
|
42
|
+
method: "GET" | "POST" | "PUT";
|
|
43
|
+
/**
|
|
44
|
+
* Read the input from the request instead of parsing JSON — for a body that
|
|
45
|
+
* is a file, passed on as a stream. Its result still goes through
|
|
46
|
+
* `inputSchema`.
|
|
47
|
+
*/
|
|
48
|
+
readInput?: (request: Request) => unknown;
|
|
43
49
|
/** Client function that calls the server — usually a `StoreClient` method. */
|
|
44
50
|
client: ProxyClientFn<TInput, TOutput>;
|
|
45
51
|
/** Input validation schema (optional for GET without query validation) */
|