@wocart/storefront 2.1.0 → 3.0.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 +85 -0
- package/USAGE.md +96 -74
- package/dist/answer-readers.js +16 -43
- package/dist/astro.d.ts +2 -2
- package/dist/astro.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/products.d.ts +2 -3
- package/dist/client/products.js +2 -3
- package/dist/endpoints/storefront-routes.js +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/middleware.d.ts +6 -26
- package/dist/middleware.js +0 -9
- package/dist/product-view.d.ts +0 -8
- package/dist/product-view.js +0 -17
- package/dist/routes.d.ts +4 -13
- package/dist/routes.js +6 -9
- package/dist/schemas.d.ts +144 -222
- package/dist/schemas.js +77 -137
- package/dist/seo.js +2 -2
- package/dist/store-request.d.ts +5 -4
- package/dist/store-request.js +1 -1
- package/dist/types.d.ts +76 -111
- package/dist/unavailable-store.d.ts +23 -0
- package/dist/unavailable-store.js +58 -0
- package/dist/utils/money.d.ts +7 -7
- package/dist/utils/money.js +8 -12
- package/dist/variant-picker.d.ts +6 -2
- package/dist/variant-picker.js +6 -9
- package/dist/whatsapp.d.ts +62 -1
- package/dist/whatsapp.js +32 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,91 @@ 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
|
+
## 3.0.0 — 2026-11-01
|
|
9
|
+
|
|
10
|
+
The SDK reads exactly what platform v7.0.0 serves: `/store/config` and the
|
|
11
|
+
product reads name every field they answer, and the SDK asks for no field the
|
|
12
|
+
server no longer sends nor tolerates an answer from an older server. The
|
|
13
|
+
WhatsApp button speaks every language, and a theme reaches it one way.
|
|
14
|
+
Requires platform v7.0.0.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `whatsAppContact(config, page, words)`: the store's WhatsApp on one page —
|
|
19
|
+
the `wa.me` link with the page's message, and every text resolved: the
|
|
20
|
+
merchant's in the page's language, else the theme's `words`; the agent the
|
|
21
|
+
merchant's, else the store's name. Null when the store has none. Every
|
|
22
|
+
WhatsApp link a theme draws comes from it. Types `WhatsAppContact`,
|
|
23
|
+
`WhatsAppPage`, `WhatsAppWords`.
|
|
24
|
+
- `unavailableStoreConfig({ serverUrl, locale })`: the store a page renders
|
|
25
|
+
when its config cannot be read — nothing to buy, its one language described
|
|
26
|
+
by the runtime. A theme passes it as `resolveStoreRequest`'s `fallback`
|
|
27
|
+
instead of writing its own.
|
|
28
|
+
- Page types `search` (`/search`) and `contact` (`/contact`); like
|
|
29
|
+
`trackOrder`, they show the widget by the App's `pages` switch.
|
|
30
|
+
- `StoreConfig.contact`: `{ phone, email, address }`, each null until the
|
|
31
|
+
merchant writes it.
|
|
32
|
+
- `CartValidation.freeDelivery.by`: why delivery is free — `offer`, `coupon`
|
|
33
|
+
or `threshold` — or null.
|
|
34
|
+
|
|
35
|
+
### Changed (breaking)
|
|
36
|
+
|
|
37
|
+
- The WhatsApp widget's texts arrive in the page's language, or null where
|
|
38
|
+
the merchant wrote none in it — never another language's.
|
|
39
|
+
- `ProductList.variantOptions` and `ProductList.tags` arrive parsed, as on a
|
|
40
|
+
product page.
|
|
41
|
+
- `Product.variants` holds only the active variants, in the merchant's
|
|
42
|
+
order: `ProductVariant` drops `productId`, `currency`, `barcode`,
|
|
43
|
+
`weightKg`, `active`, `position`, `createdAt` and `updatedAt`, and the
|
|
44
|
+
variant picker takes the variants as they come.
|
|
45
|
+
- Required, where they were optional for older servers:
|
|
46
|
+
`StoreConfig.themeSettings`, `ProductPage.total` (0 on a failed read, never
|
|
47
|
+
null), `StoreCollection.productCount` and `image`, `Offer.rewardUnitPrice`,
|
|
48
|
+
`Product.quantityPricing`, the product `metaTitle` and `metaDescription`,
|
|
49
|
+
`LandingPage.document`, `Review.verified` and `media`,
|
|
50
|
+
`ReviewSubmitResult.visibility`,
|
|
51
|
+
`CreatedOrder.couponDiscount`, a cart quote's `coupon`, a line's `offerId`,
|
|
52
|
+
`offerName` and `discount`, and a gift's `offerName`, `variantId` and
|
|
53
|
+
`variantLabel`. `ReviewInput.phone` is required.
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- `StoreConfig.primaryColor`, `accentColor`, `bgColor`, `fontFamily`,
|
|
58
|
+
`fontUrl` and `announcementBar` (a theme's settings now), `themeId`,
|
|
59
|
+
`cartShippingMode`, `status`, `createdAt`, `updatedAt`, and `legalContact`
|
|
60
|
+
(`contact` replaces it; the delivery days stay on the store's legal
|
|
61
|
+
pages).
|
|
62
|
+
- `Product.costPrice`, `storeFeatured`, `deletedAt`, `visibility`, `status`,
|
|
63
|
+
`showInStore`, `publishedAt`, `createdAt`, `updatedAt` and
|
|
64
|
+
`shippingProfileId`, and `ProductListFilters.featured` (use
|
|
65
|
+
`collection: "featured"`).
|
|
66
|
+
- `localWhatsAppWidget`, `resolvePrefillText`, `buildWhatsAppHref`,
|
|
67
|
+
`stripTrackingParams`, `truncateAtWordBoundary` and `ResolvePrefillParams`:
|
|
68
|
+
`whatsAppContact` applies the message rules itself. Removed without a
|
|
69
|
+
deprecated minor first: the platform they served changes with this major.
|
|
70
|
+
- `productTags`: both product shapes carry their tags as a list.
|
|
71
|
+
- `STOREFRONT_ROUTES` and `StorefrontRoute` (use `STOREFRONT_PAGE_PATHS`),
|
|
72
|
+
`resolveActionRedirectDecision` and `PrgDecision` (use `runFormAction`), and
|
|
73
|
+
`ActionMiddlewareDeps.referer` and `pathname`.
|
|
74
|
+
|
|
75
|
+
## 2.2.0 — 2026-11-01
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- `localWhatsAppWidget(config)`: the WhatsApp widget as a page in
|
|
80
|
+
`config.locale` shows it. Its texts are written once, in the store's
|
|
81
|
+
primary language; on a page in another language they are null, so a theme
|
|
82
|
+
shows its own words in the shopper's language instead of the merchant's in
|
|
83
|
+
another one. The agent's name and every setting stay.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- `formatMoney` writes the currency symbol after the amount in every
|
|
88
|
+
language, as Algerian prices are written (`5 000 DA`, `5,000 DA`,
|
|
89
|
+
`5.000 دج`), joined by a no-break space so the two never wrap apart. French
|
|
90
|
+
and English put it before the amount (`DA 5 000`). Without a symbol it is
|
|
91
|
+
the amount alone, with no stray space.
|
|
92
|
+
|
|
8
93
|
## 2.1.0 — 2026-10-31
|
|
9
94
|
|
|
10
95
|
### Added
|
package/USAGE.md
CHANGED
|
@@ -112,6 +112,8 @@ break those links.
|
|
|
112
112
|
| `checkout` | `/checkout` | the cart's checkout | required when the store turns the cart on |
|
|
113
113
|
| `thankYou` | `/thank-you` | the order confirmation | required |
|
|
114
114
|
| `trackOrder` | `/track-order` | where a shopper follows an order by its number and phone; `?order=<number>` fills the number in | required |
|
|
115
|
+
| `search` | `/search` | the products a shopper's words find; `?q=<words>` is the query | required — a menu can open it |
|
|
116
|
+
| `contact` | `/contact` | how to reach the store: its WhatsApp (`whatsAppContact`) and `config.contact` | required — a menu can open it |
|
|
115
117
|
|
|
116
118
|
How a theme implements them, in Astro's own terms:
|
|
117
119
|
|
|
@@ -139,8 +141,8 @@ How a theme implements them, in Astro's own terms:
|
|
|
139
141
|
`showsWidgetOn(surfaces, pathname)` is the platform's half of "should the
|
|
140
142
|
WhatsApp widget show here": the merchant switches surfaces on; the page type
|
|
141
143
|
says which surface a pathname is. `STOREFRONT_PAGE_PATHS` is the table above
|
|
142
|
-
as data.
|
|
143
|
-
|
|
144
|
+
as data. Order tracking, search and contact count as store pages: the App's
|
|
145
|
+
`pages` switch shows the widget on them.
|
|
144
146
|
|
|
145
147
|
## The five rules to remember
|
|
146
148
|
|
|
@@ -181,7 +183,7 @@ request-scoped client is the simplest safe default:
|
|
|
181
183
|
~~~ts
|
|
182
184
|
const [config, featuredProducts] = await Promise.all([
|
|
183
185
|
store.config.get(),
|
|
184
|
-
store.products.list({
|
|
186
|
+
store.products.list({ collection: "featured", limit: 8 }),
|
|
185
187
|
]);
|
|
186
188
|
~~~
|
|
187
189
|
|
|
@@ -223,9 +225,10 @@ server directly.
|
|
|
223
225
|
| `isWilayaId` | Whether a value is a wilaya id — the range `WilayaIdSchema` checks on the server, without a schema in the browser |
|
|
224
226
|
| `readShippingRates`, `readCouponOutcome` | Read in the browser what the server handed it — the rates a page carries, a quote's coupon outcome — accepting exactly what `ShippingRatesSchema` / `CouponOutcomeSchema` accept, without Zod |
|
|
225
227
|
| `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
|
|
226
|
-
| `
|
|
228
|
+
| `whatsAppContact`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | The store's WhatsApp on one page: the `wa.me` link with the page's message and every text in the page's language — the only way a theme links to WhatsApp |
|
|
227
229
|
| `storefrontPath`, `matchStorefrontPath`, `STOREFRONT_PAGE_PATHS` | The page types and their public paths: build a link, recognise a page |
|
|
228
|
-
| `showsWidgetOn
|
|
230
|
+
| `showsWidgetOn` | Which widget surface a pathname is |
|
|
231
|
+
| `unavailableStoreConfig` | The store a page renders when its config cannot be read — `resolveStoreRequest`'s `fallback` |
|
|
229
232
|
| `productImageSource` | Which photo represents a product (cover → gallery fallback) |
|
|
230
233
|
| `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
|
|
231
234
|
| `getOfferTierView` | Pure offer-tier price, compare-price, savings, and free-shipping projection; display-only, never order authority |
|
|
@@ -372,8 +375,9 @@ Read methods use safe empty results so a theme can still render its shell:
|
|
|
372
375
|
| orders.tracking(orderId) | null |
|
|
373
376
|
|
|
374
377
|
These results do not mean the theme should crash. They mean the theme should
|
|
375
|
-
show a deliberate fallback:
|
|
376
|
-
not-found page, a delivery-rate fallback, or no
|
|
378
|
+
show a deliberate fallback: the unavailable store (`unavailableStoreConfig`),
|
|
379
|
+
an empty catalog state, a not-found page, a delivery-rate fallback, or no
|
|
380
|
+
conversion event.
|
|
377
381
|
|
|
378
382
|
### When a read degrades: `onError`
|
|
379
383
|
|
|
@@ -515,29 +519,29 @@ const cartEnabled = config?.cartEnabled === true;
|
|
|
515
519
|
const currencySymbol = config?.currencySymbol ?? "دج";
|
|
516
520
|
~~~
|
|
517
521
|
|
|
518
|
-
StoreConfig
|
|
519
|
-
|
|
522
|
+
StoreConfig holds what a theme reads of the store, every merchant text in
|
|
523
|
+
the page's language (`locale`):
|
|
520
524
|
|
|
521
|
-
- name, domain
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
- lang (the primary language), locales, contentOverrides, themeSettings,
|
|
525
|
-
currency, currencySymbol, metaTitle, metaDescription, and ogImage (a
|
|
526
|
-
MediaImage);
|
|
525
|
+
- id, name, domain and logo (a MediaImage);
|
|
526
|
+
- lang (the primary language), locale, locales, currency, currencySymbol,
|
|
527
|
+
metaTitle, metaDescription, and ogImage (a MediaImage);
|
|
527
528
|
- media, the store's MediaDelivery, which every image helper takes;
|
|
528
|
-
- reviewsEnabled
|
|
529
|
-
|
|
530
|
-
- cartEnabled, freeShippingThreshold, and cartShippingMode;
|
|
529
|
+
- reviewsEnabled, cartEnabled and freeShippingThreshold;
|
|
530
|
+
- contentOverrides and themeSettings, the merchant's edits to your theme;
|
|
531
531
|
- checkoutForm policy;
|
|
532
|
-
-
|
|
532
|
+
- whatsapp, the WhatsApp button App — read it through `whatsAppContact`;
|
|
533
533
|
- pixelId and conversionEvent;
|
|
534
|
-
-
|
|
534
|
+
- pages (the footer's page links) and contact (`{ phone, email, address }`,
|
|
535
|
+
each null until the merchant writes it);
|
|
536
|
+
- OTP and Turnstile settings.
|
|
537
|
+
|
|
538
|
+
A store's colours, fonts and announcement are your theme's settings, never
|
|
539
|
+
config fields: declare them with `defineTheme({ settings })`.
|
|
535
540
|
|
|
536
541
|
`themeSettings` holds the merchant's values for your theme's settings (the
|
|
537
542
|
ones it declares with `defineTheme({ settings })` in `@wocart/theme`), by
|
|
538
543
|
setting key, exactly as saved. It is sent only to a client created with
|
|
539
|
-
`theme`, is `{}` when the store saved none
|
|
540
|
-
predates theme settings — `resolveThemeSettings` takes either. Never read it raw: resolve it
|
|
544
|
+
`theme`, and is `{}` when the store saved none. Never read it raw: resolve it
|
|
541
545
|
against your declaration, which checks every value and falls back to your
|
|
542
546
|
default for one it no longer accepts:
|
|
543
547
|
|
|
@@ -767,18 +771,17 @@ const onSale = await store.products.list({ onSale: true, limit: 8 });
|
|
|
767
771
|
`store.collections.get(handle)` — title and description in the client's
|
|
768
772
|
language, `null` for an unknown handle (answer a real 404) — and list it
|
|
769
773
|
with `products.list({ collection: handle })`.
|
|
770
|
-
-
|
|
771
|
-
|
|
774
|
+
- The store's featured products are its `featured` collection:
|
|
775
|
+
`products.list({ collection: "featured" })`.
|
|
772
776
|
|
|
773
777
|
A collection's page also has its size and a photo: `collections.get(handle)`
|
|
774
778
|
answers `productCount` (what a shopper sees in it) and `image` (its first
|
|
775
|
-
listed product's first photo, a `ProductImage
|
|
776
|
-
|
|
779
|
+
listed product's first photo, a `ProductImage`, null while it lists none with
|
|
780
|
+
one).
|
|
777
781
|
|
|
778
782
|
**Pages, totals and order.** `products.page(filters)` answers one page,
|
|
779
783
|
whether another follows, and how many products the whole listing holds
|
|
780
|
-
(`total
|
|
781
|
-
or "36 products":
|
|
784
|
+
(`total`) — enough for "page 2 of 5" or "36 products":
|
|
782
785
|
|
|
783
786
|
~~~ts
|
|
784
787
|
const page = await store.products.page({ collection: "best-sellers", sort: "price-ascending", limit: 24, offset: 24 });
|
|
@@ -791,15 +794,13 @@ const page = await store.products.page({ collection: "best-sellers", sort: "pric
|
|
|
791
794
|
`DEFAULT_COLLECTION_HANDLES` names the collections every store starts with,
|
|
792
795
|
the likeliest to hold products first, for a page that needs one and was named
|
|
793
796
|
none. Without one a listing keeps
|
|
794
|
-
its own order: a collection's, a search's best match first, else newest.
|
|
795
|
-
server that predates sorting ignores it.
|
|
797
|
+
its own order: a collection's, a search's best match first, else newest.
|
|
796
798
|
|
|
797
799
|
**Search.** `products.search(text, filters)` answers a page of the products
|
|
798
800
|
whose name — in the client's language or the catalog's — or tags hold every
|
|
799
801
|
word, with the same filters, sort and paging; names that hold every word come
|
|
800
|
-
first. Blank text finds nothing without asking
|
|
801
|
-
|
|
802
|
-
never the whole catalog:
|
|
802
|
+
first. Blank text finds nothing without asking, and a failed search answers an
|
|
803
|
+
empty page (reported through `onError`), never the whole catalog:
|
|
803
804
|
|
|
804
805
|
~~~ts
|
|
805
806
|
const results = await store.products.search(Astro.url.searchParams.get("q") ?? "", { limit: 24 });
|
|
@@ -807,9 +808,10 @@ const results = await store.products.search(Astro.url.searchParams.get("q") ?? "
|
|
|
807
808
|
|
|
808
809
|
Product lists return ProductList objects. ProductList and Product are
|
|
809
810
|
different shapes: a list item is not a detail object and must not be cast to
|
|
810
|
-
Product.
|
|
811
|
-
|
|
812
|
-
|
|
811
|
+
Product. Both carry the same product fields — variantOptions and tags parsed,
|
|
812
|
+
metaTitle, metaDescription and reviewStats — and a ProductList adds its
|
|
813
|
+
coverImage, a Product its photos, active variants (in the merchant's order),
|
|
814
|
+
offers, quantity pricing and category.
|
|
813
815
|
|
|
814
816
|
The limit is optional. An explicit limit of 0 is preserved, so do not replace
|
|
815
817
|
it with a truthiness fallback.
|
|
@@ -1679,8 +1681,7 @@ with the error in place.
|
|
|
1679
1681
|
Never redirect from the middleware, on success or on error. Astro 7 keeps an
|
|
1680
1682
|
action result for the response being rendered only, so a redirect arrives at
|
|
1681
1683
|
the next page with no result: the thank-you redirect never fires, and a
|
|
1682
|
-
refused order shows no message.
|
|
1683
|
-
name, is deprecated and now always answers next.
|
|
1684
|
+
refused order shows no message.
|
|
1684
1685
|
|
|
1685
1686
|
### The request's store and language
|
|
1686
1687
|
|
|
@@ -1698,7 +1699,8 @@ theme builds its own context from it:
|
|
|
1698
1699
|
// src/middleware.ts
|
|
1699
1700
|
import { resolvePreviewStore, resolveStoreRequest, runFormAction } from "@wocart/storefront/astro";
|
|
1700
1701
|
|
|
1701
|
-
|
|
1702
|
+
// The theme's one client factory, and the store it renders when the config cannot be read.
|
|
1703
|
+
const options = { client: createStore, fallback: unavailableStoreConfig({ serverUrl: env.COD_SERVER_URL }) };
|
|
1702
1704
|
|
|
1703
1705
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
1704
1706
|
if (context.url.pathname === THEME_PREVIEW_PATH) {
|
|
@@ -1717,7 +1719,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1717
1719
|
|
|
1718
1720
|
`client(locale?)` makes the request's client, in a language or the
|
|
1719
1721
|
store's primary; `fallback` is the config rendered when the store cannot be
|
|
1720
|
-
read
|
|
1722
|
+
read. `unavailableStoreConfig({ serverUrl, locale })` is that config, in your
|
|
1723
|
+
theme's default language (the platform's, Arabic, when you name none): no
|
|
1724
|
+
name, nothing to buy — cart, checkout, code by SMS, reviews and WhatsApp off —
|
|
1725
|
+
and an empty `id`, by which a theme tells it from a real store. It always has
|
|
1726
|
+
the config's whole shape, so never write your own. `embedded(pathname)` overrides which
|
|
1721
1727
|
paths serve part of a page. `resolvePreviewStore(draft, options)` is the
|
|
1722
1728
|
editor's preview: the draft's language when the store serves it, the draft's
|
|
1723
1729
|
settings and text over the saved ones, tracking off.
|
|
@@ -1919,43 +1925,59 @@ image; `null` means the product genuinely has no photograph, so that is the
|
|
|
1919
1925
|
moment to render a placeholder. The list model carries no gallery at all, so a
|
|
1920
1926
|
card gets the cover or `null`.
|
|
1921
1927
|
|
|
1922
|
-
## WhatsApp
|
|
1928
|
+
## WhatsApp
|
|
1923
1929
|
|
|
1924
|
-
The
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1930
|
+
The WhatsApp button App owns the store's WhatsApp: its number, its texts,
|
|
1931
|
+
which pages show the floating launcher, how it looks. Your theme owns only
|
|
1932
|
+
how it is drawn. Every link a theme draws to WhatsApp — the launcher, a
|
|
1933
|
+
footer or menu link, a contact card, the thank-you and tracking buttons —
|
|
1934
|
+
comes from one function, `whatsAppContact(config, page, words)`, and none is
|
|
1935
|
+
written by hand:
|
|
1928
1936
|
|
|
1929
1937
|
~~~ts
|
|
1930
|
-
import {
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
- `
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
`
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1938
|
+
import { showsWidgetOn, whatsAppContact } from "@wocart/storefront";
|
|
1939
|
+
|
|
1940
|
+
const whatsapp = whatsAppContact(
|
|
1941
|
+
config,
|
|
1942
|
+
{ kind: "product", url: canonical, product: product.name },
|
|
1943
|
+
{
|
|
1944
|
+
launcher: null, // the launcher shows its icon alone unless the merchant names it
|
|
1945
|
+
caption: t("whatsapp.caption"),
|
|
1946
|
+
welcome: t("whatsapp.welcome"),
|
|
1947
|
+
cta: t("whatsapp.cta"),
|
|
1948
|
+
orderButton: t("whatsapp.order"),
|
|
1949
|
+
messages: { general: t("whatsapp.message"), product: t("whatsapp.messageProduct"), order: t("whatsapp.messageOrder") },
|
|
1950
|
+
},
|
|
1951
|
+
);
|
|
1952
|
+
const launcherShown = whatsapp !== null && showsWidgetOn(whatsapp.surfaces, store.pathname);
|
|
1953
|
+
// whatsapp.href, whatsapp.ctaLabel, whatsapp.welcomeMessage, whatsapp.agentName …
|
|
1954
|
+
~~~
|
|
1955
|
+
|
|
1956
|
+
It answers `null` when the store has no WhatsApp (the App is off, or has no
|
|
1957
|
+
usable number). Otherwise every text is resolved for this page:
|
|
1958
|
+
|
|
1959
|
+
- **The merchant's text, else your words.** `/store/config` serves each of
|
|
1960
|
+
the App's texts in the page's language, or null where the merchant wrote
|
|
1961
|
+
none in it — never another language's. A null reads your `words`, which
|
|
1962
|
+
your theme has in every language it speaks. `caption`, `welcome` and
|
|
1963
|
+
`launcher` may be null in your words too: the panel then shows nothing
|
|
1964
|
+
there.
|
|
1965
|
+
- **The page's message.** `page.kind` is `product` (a product or landing
|
|
1966
|
+
page, with `product` its name), `order` (the thank-you or tracking page,
|
|
1967
|
+
with `order` its number) or `general`. The merchant's template for that
|
|
1968
|
+
kind is used, else their general one, else yours for that kind.
|
|
1969
|
+
- **The agent** is the merchant's, else the store's name.
|
|
1970
|
+
|
|
1971
|
+
The message follows three rules no theme re-implements: it is capped at
|
|
1972
|
+
`MAX_RESOLVED_PREFILL_LENGTH` characters, cut at a word boundary, because
|
|
1973
|
+
browsers and WhatsApp both truncate silently past a point; `{url}` never
|
|
1974
|
+
carries ad-click parameters (`utm_*`, `fbclid`); and the encoded `text=` keeps
|
|
1975
|
+
a query string the number's link already carries.
|
|
1976
|
+
|
|
1977
|
+
`PREFILL_TOKENS` is the token vocabulary the dashboard offers (`product`,
|
|
1978
|
+
`url`, `order`, `store`); an unknown placeholder is left verbatim so a
|
|
1979
|
+
template typo is visible rather than silently swallowed. The cap and the
|
|
1980
|
+
token list are the same ones the dashboard enforces.
|
|
1959
1981
|
|
|
1960
1982
|
## The sitemap
|
|
1961
1983
|
|
package/dist/answer-readers.js
CHANGED
|
@@ -2,59 +2,33 @@ const isObject = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
2
2
|
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3
3
|
const isInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
|
|
4
4
|
const isNullableString = (value) => value === null || typeof value === "string";
|
|
5
|
-
/** An optional field the schema reads as `.nullish()`: absent stays absent, null and the type are kept, anything else refuses. */
|
|
6
|
-
function nullish(source, key, accepts) {
|
|
7
|
-
if (!(key in source) || source[key] === undefined)
|
|
8
|
-
return { ok: true, entry: {} };
|
|
9
|
-
const value = source[key];
|
|
10
|
-
return value === null || accepts(value) ? { ok: true, entry: { [key]: value } } : { ok: false, entry: {} };
|
|
11
|
-
}
|
|
12
|
-
const isString = (value) => typeof value === "string";
|
|
13
5
|
const BLOCKERS = ["missing", "unlisted", "out_of_stock"];
|
|
6
|
+
const FREE_BY = ["offer", "coupon", "threshold"];
|
|
14
7
|
function readQuoteLine(value) {
|
|
15
8
|
if (!isObject(value))
|
|
16
9
|
return null;
|
|
17
|
-
const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, maxQuantity, blocker } = value;
|
|
10
|
+
const { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker } = value;
|
|
18
11
|
if (typeof productId !== "string" || !isNullableString(variantId) || !isNullableString(variantLabel) || typeof productName !== "string")
|
|
19
12
|
return null;
|
|
20
13
|
if (!isInteger(quantity) || !isFiniteNumber(unitPrice) || !isFiniteNumber(lineTotal))
|
|
21
14
|
return null;
|
|
15
|
+
if (!isNullableString(offerId) || !isNullableString(offerName) || !isFiniteNumber(discount))
|
|
16
|
+
return null;
|
|
22
17
|
if (maxQuantity !== null && !isFiniteNumber(maxQuantity))
|
|
23
18
|
return null;
|
|
24
19
|
if (blocker !== null && !(typeof blocker === "string" && BLOCKERS.includes(blocker)))
|
|
25
20
|
return null;
|
|
26
|
-
|
|
27
|
-
const offerName = nullish(value, "offerName", isString);
|
|
28
|
-
const discount = nullish(value, "discount", isFiniteNumber);
|
|
29
|
-
if (!offerId.ok || !offerName.ok || !discount.ok)
|
|
30
|
-
return null;
|
|
31
|
-
return {
|
|
32
|
-
productId,
|
|
33
|
-
variantId,
|
|
34
|
-
variantLabel,
|
|
35
|
-
productName,
|
|
36
|
-
quantity,
|
|
37
|
-
unitPrice,
|
|
38
|
-
lineTotal,
|
|
39
|
-
...offerId.entry,
|
|
40
|
-
...offerName.entry,
|
|
41
|
-
...discount.entry,
|
|
42
|
-
maxQuantity,
|
|
43
|
-
blocker: blocker,
|
|
44
|
-
};
|
|
21
|
+
return { productId, variantId, variantLabel, productName, quantity, unitPrice, lineTotal, offerId, offerName, discount, maxQuantity, blocker: blocker };
|
|
45
22
|
}
|
|
46
23
|
function readReward(value) {
|
|
47
24
|
if (!isObject(value))
|
|
48
25
|
return null;
|
|
49
|
-
const { offerId, productId, productName, quantity } = value;
|
|
50
|
-
if (typeof offerId !== "string" || !isNullableString(
|
|
26
|
+
const { offerId, offerName, productId, productName, variantId, variantLabel, quantity } = value;
|
|
27
|
+
if (typeof offerId !== "string" || !isNullableString(offerName) || !isNullableString(productId) || !isNullableString(productName))
|
|
51
28
|
return null;
|
|
52
|
-
|
|
53
|
-
const variantId = nullish(value, "variantId", isString);
|
|
54
|
-
const variantLabel = nullish(value, "variantLabel", isString);
|
|
55
|
-
if (!offerName.ok || !variantId.ok || !variantLabel.ok)
|
|
29
|
+
if (!isNullableString(variantId) || !isNullableString(variantLabel) || !isInteger(quantity))
|
|
56
30
|
return null;
|
|
57
|
-
return { offerId,
|
|
31
|
+
return { offerId, offerName, productId, productName, variantId, variantLabel, quantity };
|
|
58
32
|
}
|
|
59
33
|
function readDelivery(value) {
|
|
60
34
|
if (!isObject(value))
|
|
@@ -93,25 +67,24 @@ export function readCartQuote(value) {
|
|
|
93
67
|
return null;
|
|
94
68
|
if (free.threshold !== null && !isFiniteNumber(free.threshold))
|
|
95
69
|
return null;
|
|
70
|
+
if (free.by !== null && !(typeof free.by === "string" && FREE_BY.includes(free.by)))
|
|
71
|
+
return null;
|
|
96
72
|
let delivery = null;
|
|
97
73
|
if ("delivery" in value && value.delivery !== undefined) {
|
|
98
74
|
delivery = readDelivery(value.delivery);
|
|
99
75
|
if (delivery === null)
|
|
100
76
|
return null;
|
|
101
77
|
}
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
if (coupon === null && value.coupon !== null)
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
78
|
+
const coupon = value.coupon === null ? null : readCouponOutcome(value.coupon);
|
|
79
|
+
if (coupon === null && value.coupon !== null)
|
|
80
|
+
return null;
|
|
108
81
|
return {
|
|
109
82
|
lines,
|
|
110
83
|
subtotal: value.subtotal,
|
|
111
84
|
rewards,
|
|
112
|
-
freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining },
|
|
85
|
+
freeDelivery: { fromOffer: free.fromOffer, threshold: free.threshold, qualified: free.qualified, remaining: free.remaining, by: free.by },
|
|
113
86
|
...(delivery === null ? {} : { delivery }),
|
|
114
|
-
|
|
87
|
+
coupon,
|
|
115
88
|
};
|
|
116
89
|
}
|
|
117
90
|
/** The delivery rates of a value (`ShippingRates`), or null when it is not a rate table. */
|
package/dist/astro.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from
|
|
|
4
4
|
export type { StorefrontApiRoute, StorefrontRoutes, StorefrontRoutesOptions, } from "./endpoints/storefront-routes.js";
|
|
5
5
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
6
6
|
export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
|
|
7
|
-
export { runFormAction
|
|
8
|
-
export type {
|
|
7
|
+
export { runFormAction } from "./middleware.js";
|
|
8
|
+
export type { ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
|
|
9
9
|
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
10
10
|
export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
|
|
11
11
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
package/dist/astro.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createProxyEndpoint } from "./endpoints/proxy.js";
|
|
2
2
|
export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
|
|
3
3
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
4
|
-
export { runFormAction
|
|
4
|
+
export { runFormAction } from "./middleware.js";
|
|
5
5
|
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
6
6
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
|
@@ -41,9 +41,8 @@ export interface ProductsNamespace {
|
|
|
41
41
|
* The products a shopper's words find — every word in the product's name,
|
|
42
42
|
* in the client's language or the catalog's, or in its tags — as a page,
|
|
43
43
|
* with the listing filters, sort and paging. Without a `sort`, products
|
|
44
|
-
* whose name holds every word come first. Blank words find nothing
|
|
45
|
-
*
|
|
46
|
-
* `onError`), never the whole catalog.
|
|
44
|
+
* whose name holds every word come first. Blank words find nothing, never
|
|
45
|
+
* the whole catalog.
|
|
47
46
|
*/
|
|
48
47
|
search(text: string, filters?: ProductListFilters): Promise<ProductPage>;
|
|
49
48
|
/**
|
package/dist/client/products.js
CHANGED
|
@@ -29,7 +29,6 @@ export function createProductsNamespace(core) {
|
|
|
29
29
|
categoryId: filters?.categoryId || undefined,
|
|
30
30
|
collection: filters?.collection || undefined,
|
|
31
31
|
onSale: filters?.onSale ? "true" : undefined,
|
|
32
|
-
featured: filters?.featured ? "true" : undefined,
|
|
33
32
|
sort: filters?.sort || undefined,
|
|
34
33
|
limit: filters?.limit === undefined ? undefined : String(filters.limit),
|
|
35
34
|
offset: filters?.offset ? String(filters.offset) : undefined,
|
|
@@ -53,11 +52,11 @@ export function createProductsNamespace(core) {
|
|
|
53
52
|
operation,
|
|
54
53
|
});
|
|
55
54
|
const rows = envelope?.data ?? [];
|
|
56
|
-
return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ??
|
|
55
|
+
return { items: rows.slice(0, size), hasNext: rows.length > size, total: envelope?.total ?? 0 };
|
|
57
56
|
}
|
|
58
57
|
catch (error) {
|
|
59
58
|
reportDegradedRead(core.config, error, operation);
|
|
60
|
-
return { items: [], hasNext: false, total:
|
|
59
|
+
return { items: [], hasNext: false, total: 0 };
|
|
61
60
|
}
|
|
62
61
|
};
|
|
63
62
|
return {
|
|
@@ -75,7 +75,7 @@ const reviewsListInput = z.object({
|
|
|
75
75
|
*/
|
|
76
76
|
const reviewSubmitInput = z.object({
|
|
77
77
|
orderNumber: z.string().min(1).max(64),
|
|
78
|
-
phone: z.string().min(1).max(32)
|
|
78
|
+
phone: z.string().min(1).max(32),
|
|
79
79
|
productId: z.string().min(1).max(200),
|
|
80
80
|
rating: z.number(),
|
|
81
81
|
title: z.string().max(500).optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -15,13 +15,15 @@ export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from
|
|
|
15
15
|
export type { MediaDelivery, MediaDeliveryKind, MediaImage, MediaImageParams, MediaImageSrcsetOptions, PickedImage, Picture } from "./utils/image.js";
|
|
16
16
|
export { productImageSource } from "./utils/product-image.js";
|
|
17
17
|
export type { ProductImageSourceInput } from "./utils/product-image.js";
|
|
18
|
-
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS,
|
|
19
|
-
export type { PrefillToken,
|
|
20
|
-
export { STOREFRONT_PAGE_PATHS,
|
|
18
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
19
|
+
export type { PrefillToken, WhatsAppContact, WhatsAppPage, WhatsAppWords } from "./whatsapp.js";
|
|
20
|
+
export { STOREFRONT_PAGE_PATHS, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
|
|
21
21
|
export { servedLanguage } from "./locales.js";
|
|
22
|
+
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
23
|
+
export type { UnavailableStoreOptions } from "./unavailable-store.js";
|
|
22
24
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
23
25
|
export type { LocalePath, LocaleRouting } from "./locale-routing.js";
|
|
24
|
-
export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams
|
|
26
|
+
export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams } from "./routes.js";
|
|
25
27
|
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
26
28
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
27
29
|
export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
export { formatMoney } from "./utils/money.js";
|
|
13
13
|
export { isMediaKey, mediaImageSrcset, mediaImageUrl, mediaUrl, pictureOf } from "./utils/image.js";
|
|
14
14
|
export { productImageSource } from "./utils/product-image.js";
|
|
15
|
-
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS,
|
|
16
|
-
export { STOREFRONT_PAGE_PATHS,
|
|
15
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, whatsAppContact } from "./whatsapp.js";
|
|
16
|
+
export { STOREFRONT_PAGE_PATHS, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
|
|
17
17
|
export { servedLanguage } from "./locales.js";
|
|
18
|
+
export { unavailableStoreConfig } from "./unavailable-store.js";
|
|
18
19
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
19
20
|
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
20
21
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|