@wocart/storefront 2.2.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 +67 -0
- package/USAGE.md +96 -83
- 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/variant-picker.d.ts +6 -2
- package/dist/variant-picker.js +6 -9
- package/dist/whatsapp.d.ts +59 -6
- package/dist/whatsapp.js +28 -10
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -64,38 +64,16 @@ export interface StorefrontLanguage {
|
|
|
64
64
|
/** `<html dir>` for this language. */
|
|
65
65
|
direction: "rtl" | "ltr";
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The storefront's view of the store, in one language — `config.get()`.
|
|
69
|
+
* Every merchant text is in `locale`; a field holds exactly what a theme
|
|
70
|
+
* reads, nothing the server keeps to itself.
|
|
71
|
+
*/
|
|
67
72
|
export interface StoreConfig {
|
|
68
73
|
id: string;
|
|
69
74
|
name: string;
|
|
70
75
|
domain: string | null;
|
|
71
76
|
logo: MediaImage | null;
|
|
72
|
-
themeId: string;
|
|
73
|
-
/**
|
|
74
|
-
* @deprecated A store's colors and font are its theme's settings: declare
|
|
75
|
-
* them in `defineTheme({ settings })` and read `themeSettings`. This is the
|
|
76
|
-
* value saved before that, no longer editable; removed in 2.0.
|
|
77
|
-
*/
|
|
78
|
-
primaryColor: string;
|
|
79
|
-
/**
|
|
80
|
-
* @deprecated A store's colors and font are its theme's settings: declare
|
|
81
|
-
* them in `defineTheme({ settings })` and read `themeSettings`. This is the
|
|
82
|
-
* value saved before that, no longer editable; removed in 2.0.
|
|
83
|
-
*/
|
|
84
|
-
accentColor: string;
|
|
85
|
-
/**
|
|
86
|
-
* @deprecated A store's colors and font are its theme's settings: declare
|
|
87
|
-
* them in `defineTheme({ settings })` and read `themeSettings`. This is the
|
|
88
|
-
* value saved before that, no longer editable; removed in 2.0.
|
|
89
|
-
*/
|
|
90
|
-
bgColor: string;
|
|
91
|
-
/**
|
|
92
|
-
* @deprecated A store's colors and font are its theme's settings: declare
|
|
93
|
-
* them in `defineTheme({ settings })` and read `themeSettings`. This is the
|
|
94
|
-
* value saved before that, no longer editable; removed in 2.0.
|
|
95
|
-
*/
|
|
96
|
-
fontFamily: string;
|
|
97
|
-
/** @deprecated Always null; removed in 2.0. */
|
|
98
|
-
fontUrl: string | null;
|
|
99
77
|
/** The store's primary language: a BCP 47 code such as `ar`. */
|
|
100
78
|
lang: string;
|
|
101
79
|
/**
|
|
@@ -105,23 +83,8 @@ export interface StoreConfig {
|
|
|
105
83
|
locale: string;
|
|
106
84
|
/** The published languages, primary first (always includes `lang`). */
|
|
107
85
|
locales: StorefrontLanguage[];
|
|
108
|
-
/**
|
|
109
|
-
* The store's edits to this theme's text, one map per published language:
|
|
110
|
-
* `{ ar: { heroTitle: "…" } }`. Sent only to a client created with `theme`.
|
|
111
|
-
* A theme merges `contentOverrides[locale]` over its own pack for that
|
|
112
|
-
* language — never another language's map.
|
|
113
|
-
*/
|
|
114
|
-
contentOverrides: Record<string, Record<string, string>>;
|
|
115
|
-
/**
|
|
116
|
-
* The merchant's values for this theme's settings, by setting key, exactly
|
|
117
|
-
* as saved — `{}` when there are none or the client was created without
|
|
118
|
-
* `theme`, absent from a server that predates theme settings. Read them
|
|
119
|
-
* against the theme's declaration with `resolveThemeSettings` from
|
|
120
|
-
* `@wocart/theme` (which takes either): a value the theme no longer accepts
|
|
121
|
-
* falls back to its default there.
|
|
122
|
-
*/
|
|
123
|
-
themeSettings?: Record<string, unknown> | undefined;
|
|
124
86
|
currency: string;
|
|
87
|
+
/** In `locale`: "دج" in Arabic reads "DA" in French. */
|
|
125
88
|
currencySymbol: string;
|
|
126
89
|
metaTitle: string | null;
|
|
127
90
|
metaDescription: string | null;
|
|
@@ -129,47 +92,55 @@ export interface StoreConfig {
|
|
|
129
92
|
ogImage: MediaImage | null;
|
|
130
93
|
/** Where every image key is served from, and how it is resized: pass it to `mediaUrl`, `mediaImageUrl` and `pictureOf`. */
|
|
131
94
|
media: MediaDelivery;
|
|
132
|
-
/**
|
|
133
|
-
* @deprecated Always null. An announcement is a theme's own content: declare
|
|
134
|
-
* it as a theme setting and read `themeSettings`. Removed in 2.0.
|
|
135
|
-
*/
|
|
136
|
-
announcementBar: string | null;
|
|
137
95
|
reviewsEnabled: boolean;
|
|
138
96
|
/** When true the storefront renders the cart alongside the direct order form. */
|
|
139
97
|
cartEnabled: boolean;
|
|
140
98
|
/** Subtotal (DZD) at or above which delivery is free. null = no threshold. */
|
|
141
99
|
freeShippingThreshold: number | null;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The store's edits to this theme's text, one map per published language:
|
|
102
|
+
* `{ ar: { heroTitle: "…" } }`. Sent only to a client created with `theme`.
|
|
103
|
+
* A theme merges `contentOverrides[locale]` over its own pack for that
|
|
104
|
+
* language — never another language's map.
|
|
105
|
+
*/
|
|
106
|
+
contentOverrides: Record<string, Record<string, string>>;
|
|
107
|
+
/**
|
|
108
|
+
* The merchant's values for this theme's settings, by setting key, exactly
|
|
109
|
+
* as saved — `{}` when there are none or the client was created without
|
|
110
|
+
* `theme`. Read them against the theme's declaration with
|
|
111
|
+
* `resolveThemeSettings` from `@wocart/theme`: a value the theme no longer
|
|
112
|
+
* accepts falls back to its default there.
|
|
113
|
+
*/
|
|
114
|
+
themeSettings: Record<string, unknown>;
|
|
151
115
|
checkoutForm: CheckoutFormPolicy;
|
|
116
|
+
/** The WhatsApp button, or null when the store has none. Read it through `whatsAppContact`. */
|
|
117
|
+
whatsapp: StoreWhatsAppWidget | null;
|
|
152
118
|
pixelId: string | null;
|
|
153
119
|
conversionEvent: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead";
|
|
154
120
|
/**
|
|
155
|
-
* Published pages that opt into the footer, titled in
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
121
|
+
* Published pages that opt into the footer, titled in `locale`, ordered for
|
|
122
|
+
* display. The checkout consent line resolves Terms/Refund from here by
|
|
123
|
+
* `kind` — never a hardcoded slug, since a merchant may rename any page's
|
|
124
|
+
* slug freely.
|
|
159
125
|
*/
|
|
160
126
|
pages: StorePageLink[];
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
whatsapp: StoreWhatsAppWidget | null;
|
|
127
|
+
/** How a shopper reaches the store, besides WhatsApp: each null until the merchant writes it. */
|
|
128
|
+
contact: StoreContact;
|
|
129
|
+
otpEnabled: boolean;
|
|
130
|
+
/** Cloudflare Turnstile — true only when the store switched it on. */
|
|
131
|
+
turnstileEnabled: boolean;
|
|
132
|
+
/** Public widget site key; null when Turnstile is off. The secret never leaves the server. */
|
|
133
|
+
turnstileSiteKey: string | null;
|
|
169
134
|
}
|
|
170
135
|
/** The merchant's per-surface switches for the WhatsApp contact widget. */
|
|
171
136
|
export type WidgetSurfaces = StoreWhatsAppWidget["surfaces"];
|
|
137
|
+
/**
|
|
138
|
+
* The WhatsApp button App, as the server serves it: each text in the page's
|
|
139
|
+
* language, or null where the merchant wrote none in it — the theme's words
|
|
140
|
+
* then. A theme reads it through `whatsAppContact`, never field by field.
|
|
141
|
+
*/
|
|
172
142
|
export interface StoreWhatsAppWidget {
|
|
143
|
+
/** The chat link to the store's number, without a message. */
|
|
173
144
|
href: string;
|
|
174
145
|
agentName: string | null;
|
|
175
146
|
caption: string | null;
|
|
@@ -203,11 +174,11 @@ export interface StorePageLink {
|
|
|
203
174
|
title: string;
|
|
204
175
|
position: number;
|
|
205
176
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
177
|
+
/** How a shopper reaches the store — `StoreConfig.contact`. */
|
|
178
|
+
export interface StoreContact {
|
|
179
|
+
phone: string | null;
|
|
180
|
+
email: string | null;
|
|
181
|
+
address: string | null;
|
|
211
182
|
}
|
|
212
183
|
/** A resolved store page — GET /store/pages/{slug}. */
|
|
213
184
|
export interface StorePagePublic {
|
|
@@ -227,10 +198,10 @@ export interface StoreCollection {
|
|
|
227
198
|
/** In the client's language when the store translated it, else the primary. */
|
|
228
199
|
title: string;
|
|
229
200
|
description: string | null;
|
|
230
|
-
/** How many products a shopper sees in it
|
|
231
|
-
productCount
|
|
232
|
-
/** Its first listed product's first photo, until a merchant can pick one;
|
|
233
|
-
image
|
|
201
|
+
/** How many products a shopper sees in it. */
|
|
202
|
+
productCount: number;
|
|
203
|
+
/** Its first listed product's first photo, until a merchant can pick one; null while it lists none with one. */
|
|
204
|
+
image: ProductImage | null;
|
|
234
205
|
}
|
|
235
206
|
/** A navigation menu — GET /store/menus/{handle}. */
|
|
236
207
|
export interface StoreMenu {
|
|
@@ -247,7 +218,7 @@ export interface StoreMenuItem {
|
|
|
247
218
|
* it through `localizeHref` — or the absolute URL the merchant entered.
|
|
248
219
|
*/
|
|
249
220
|
href: string;
|
|
250
|
-
/**
|
|
221
|
+
/** What it opens: a page type (`home`, `catalog`, `search`, `trackOrder`, `contact`, `category`, `collection`, `product`, `page`, `landing`), or `url`. */
|
|
251
222
|
type: string;
|
|
252
223
|
children: StoreMenuItem[];
|
|
253
224
|
}
|
|
@@ -298,8 +269,8 @@ export interface LandingPage {
|
|
|
298
269
|
};
|
|
299
270
|
/**
|
|
300
271
|
* The page's own composition for the client's `theme` (its `lp:<id>`
|
|
301
|
-
* document), or null when it has none
|
|
302
|
-
*
|
|
272
|
+
* document), or null when it has none or the client names no theme: the
|
|
273
|
+
* theme then renders its `landing` default.
|
|
303
274
|
*/
|
|
304
275
|
document: PageDocumentPayload | null;
|
|
305
276
|
}
|
|
@@ -324,12 +295,12 @@ export interface CartValidationLine {
|
|
|
324
295
|
unitPrice: number;
|
|
325
296
|
/** What the line costs, offers and the coupon applied. */
|
|
326
297
|
lineTotal: number;
|
|
327
|
-
/** The offer applied to the line; null
|
|
328
|
-
offerId
|
|
329
|
-
/** That offer's name, in the shopper's language; null
|
|
330
|
-
offerName
|
|
331
|
-
/** The line's share of the coupon's discount, already out of `lineTotal`;
|
|
332
|
-
discount
|
|
298
|
+
/** The offer applied to the line; null when none. */
|
|
299
|
+
offerId: string | null;
|
|
300
|
+
/** That offer's name, in the shopper's language; null when none. */
|
|
301
|
+
offerName: string | null;
|
|
302
|
+
/** The line's share of the coupon's discount, already out of `lineTotal`; 0 without one. */
|
|
303
|
+
discount: number;
|
|
333
304
|
/** Units still available; null when the product is not stock-tracked. */
|
|
334
305
|
maxQuantity: number | null;
|
|
335
306
|
/**
|
|
@@ -338,17 +309,16 @@ export interface CartValidationLine {
|
|
|
338
309
|
*/
|
|
339
310
|
blocker: "missing" | "unlisted" | "out_of_stock" | null;
|
|
340
311
|
}
|
|
341
|
-
/** A satisfied Buy-X-Get-Y reward, appended server-side at order time. */
|
|
342
312
|
/** A gift the basket earns, as the order will give it: free, beside the lines. */
|
|
343
313
|
export interface CartReward {
|
|
344
314
|
offerId: string;
|
|
345
|
-
/** The offer's name, in the shopper's language
|
|
346
|
-
offerName
|
|
315
|
+
/** The offer's name, in the shopper's language. */
|
|
316
|
+
offerName: string | null;
|
|
347
317
|
productId: string | null;
|
|
348
318
|
productName: string | null;
|
|
349
|
-
/** The variant given and its label; null for a product without variants
|
|
350
|
-
variantId
|
|
351
|
-
variantLabel
|
|
319
|
+
/** The variant given and its label; null for a product without variants. */
|
|
320
|
+
variantId: string | null;
|
|
321
|
+
variantLabel: string | null;
|
|
352
322
|
quantity: number;
|
|
353
323
|
}
|
|
354
324
|
/** The catalog's truth about a basket, right now. Read-only — nothing is written. */
|
|
@@ -362,6 +332,8 @@ export interface CartValidation {
|
|
|
362
332
|
threshold: number | null;
|
|
363
333
|
qualified: boolean;
|
|
364
334
|
remaining: number;
|
|
335
|
+
/** Why delivery is free: an offer, the coupon or the store's amount; null when it is not. */
|
|
336
|
+
by: "offer" | "coupon" | "threshold" | null;
|
|
365
337
|
};
|
|
366
338
|
/**
|
|
367
339
|
* Present when `validate` was given a destination: what the order would
|
|
@@ -371,8 +343,8 @@ export interface CartValidation {
|
|
|
371
343
|
* deliver there, and an order to it is refused.
|
|
372
344
|
*/
|
|
373
345
|
delivery?: CartDeliveryQuote | undefined;
|
|
374
|
-
/** What the code the quote was asked with did; null without one
|
|
375
|
-
coupon
|
|
346
|
+
/** What the code the quote was asked with did; null without one. */
|
|
347
|
+
coupon: CouponOutcome | null;
|
|
376
348
|
}
|
|
377
349
|
/**
|
|
378
350
|
* Why a code does not apply. Open: a server may name a reason this SDK has not
|
|
@@ -413,8 +385,8 @@ export interface CreatedOrder {
|
|
|
413
385
|
deliveryFee: number;
|
|
414
386
|
/** price + deliveryFee, offers and the coupon applied. */
|
|
415
387
|
total: number;
|
|
416
|
-
/** What the coupon took off;
|
|
417
|
-
couponDiscount
|
|
388
|
+
/** What the coupon took off; 0 without one. */
|
|
389
|
+
couponDiscount: number;
|
|
418
390
|
}
|
|
419
391
|
/** One basket line in a create-order body. */
|
|
420
392
|
export interface CreateOrderItemInput {
|
|
@@ -573,7 +545,7 @@ export interface Review {
|
|
|
573
545
|
createdAt: string;
|
|
574
546
|
/** Rests on an order that reached its customer: show it as a verified purchase. */
|
|
575
547
|
verified: boolean;
|
|
576
|
-
/** Photos first, then the voice note.
|
|
548
|
+
/** Photos first, then the voice note. */
|
|
577
549
|
media: ReviewMedia[];
|
|
578
550
|
}
|
|
579
551
|
/** One product of the shopper's order, as `products.reviews.eligibility()` answers it. */
|
|
@@ -613,8 +585,7 @@ export interface ReviewUpload {
|
|
|
613
585
|
/** What a submitted review answers: published now, or after the store checks it. */
|
|
614
586
|
export interface ReviewSubmitResult {
|
|
615
587
|
id: string;
|
|
616
|
-
|
|
617
|
-
visibility?: "published" | "after_review" | undefined;
|
|
588
|
+
visibility: "published" | "after_review";
|
|
618
589
|
}
|
|
619
590
|
/**
|
|
620
591
|
* Filters `products.list()` accepts — named so a theme can type its own
|
|
@@ -631,11 +602,9 @@ export interface ProductListFilters {
|
|
|
631
602
|
collection?: string;
|
|
632
603
|
/** Only products priced below their compare-at price. */
|
|
633
604
|
onSale?: boolean;
|
|
634
|
-
/** @deprecated Use `collection: "featured"`. */
|
|
635
|
-
featured?: boolean;
|
|
636
605
|
/**
|
|
637
606
|
* The order. Absent: the listing's own — a collection's, a search's best
|
|
638
|
-
* match first, else newest.
|
|
607
|
+
* match first, else newest.
|
|
639
608
|
*/
|
|
640
609
|
sort?: ProductSort;
|
|
641
610
|
/** Server-side cap; the server clamps anything larger. */
|
|
@@ -650,8 +619,8 @@ export interface ProductPage {
|
|
|
650
619
|
items: ProductList[];
|
|
651
620
|
/** True when another page follows this one. */
|
|
652
621
|
hasNext: boolean;
|
|
653
|
-
/** How many products the whole listing holds, across its pages
|
|
654
|
-
total: number
|
|
622
|
+
/** How many products the whole listing holds, across its pages. */
|
|
623
|
+
total: number;
|
|
655
624
|
}
|
|
656
625
|
/** `products.reviews.list()` paging — named for the same reason. */
|
|
657
626
|
export interface ReviewListPagination {
|
|
@@ -665,12 +634,8 @@ export interface ReviewListPagination {
|
|
|
665
634
|
*/
|
|
666
635
|
export interface ReviewInput {
|
|
667
636
|
orderNumber: string;
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
* is text only and still needs a delivered order.
|
|
671
|
-
* @deprecated Omitting it is deprecated; a later major requires it.
|
|
672
|
-
*/
|
|
673
|
-
phone?: string;
|
|
637
|
+
/** The phone the order was placed with. */
|
|
638
|
+
phone: string;
|
|
674
639
|
productId: string;
|
|
675
640
|
/** 1–5, the server validates the range. */
|
|
676
641
|
rating: number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store a page renders when its config cannot be read.
|
|
3
|
+
*
|
|
4
|
+
* Every theme needs one, and each used to write its own — with whatever
|
|
5
|
+
* fields the config had when it was written. It is the SDK's, so it always has
|
|
6
|
+
* the config's shape, and it decides once what a store we cannot read offers:
|
|
7
|
+
* nothing to buy. The cart, the checkout, the code by SMS, reviews and the
|
|
8
|
+
* WhatsApp button are off, so the page can never look like a working
|
|
9
|
+
* checkout; the theme renders its own words in its layout around it.
|
|
10
|
+
*/
|
|
11
|
+
import type { StoreConfig } from "./types.js";
|
|
12
|
+
export interface UnavailableStoreOptions {
|
|
13
|
+
/** The store's server, as the theme's client is given it: a store that serves its own files serves them from its `/media`. */
|
|
14
|
+
serverUrl: string;
|
|
15
|
+
/** The page's language. Default: the platform's default language. */
|
|
16
|
+
locale?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The config of a store that cannot be read, in `locale`, its only language —
|
|
20
|
+
* pass it as `resolveStoreRequest`'s `fallback`. Its `id` is empty: no store
|
|
21
|
+
* has that id, so a theme tells it from a real config by `config.id`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function unavailableStoreConfig({ serverUrl, locale }: UnavailableStoreOptions): StoreConfig;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The platform's default store language: a new store speaks it until the merchant picks another. */
|
|
2
|
+
const DEFAULT_LOCALE = "ar";
|
|
3
|
+
/**
|
|
4
|
+
* A language described by the runtime's own data: the platform's list of
|
|
5
|
+
* languages lives on the server, and an unreadable store has none to send.
|
|
6
|
+
*/
|
|
7
|
+
function describedLanguage(code) {
|
|
8
|
+
const locale = new Intl.Locale(code);
|
|
9
|
+
const info = locale.getTextInfo?.() ?? locale.textInfo;
|
|
10
|
+
const name = new Intl.DisplayNames([code], { type: "language" }).of(code) ?? code;
|
|
11
|
+
return {
|
|
12
|
+
code,
|
|
13
|
+
nativeName: name.charAt(0).toLocaleUpperCase(code) + name.slice(1),
|
|
14
|
+
direction: info?.direction === "rtl" ? "rtl" : "ltr",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The config of a store that cannot be read, in `locale`, its only language —
|
|
19
|
+
* pass it as `resolveStoreRequest`'s `fallback`. Its `id` is empty: no store
|
|
20
|
+
* has that id, so a theme tells it from a real config by `config.id`.
|
|
21
|
+
*/
|
|
22
|
+
export function unavailableStoreConfig({ serverUrl, locale = DEFAULT_LOCALE }) {
|
|
23
|
+
return {
|
|
24
|
+
id: "",
|
|
25
|
+
name: "",
|
|
26
|
+
domain: null,
|
|
27
|
+
logo: null,
|
|
28
|
+
lang: locale,
|
|
29
|
+
locale,
|
|
30
|
+
locales: [describedLanguage(locale)],
|
|
31
|
+
currency: "DZD",
|
|
32
|
+
currencySymbol: "DZD",
|
|
33
|
+
metaTitle: null,
|
|
34
|
+
metaDescription: null,
|
|
35
|
+
ogImage: null,
|
|
36
|
+
media: { kind: "direct", origin: `${serverUrl.replace(/\/+$/, "")}/media` },
|
|
37
|
+
reviewsEnabled: false,
|
|
38
|
+
cartEnabled: false,
|
|
39
|
+
freeShippingThreshold: null,
|
|
40
|
+
contentOverrides: {},
|
|
41
|
+
themeSettings: {},
|
|
42
|
+
checkoutForm: {
|
|
43
|
+
address: "hidden",
|
|
44
|
+
notes: "hidden",
|
|
45
|
+
email: "hidden",
|
|
46
|
+
deliveryOptions: { home: false, stopDesk: false },
|
|
47
|
+
customFields: [],
|
|
48
|
+
},
|
|
49
|
+
whatsapp: null,
|
|
50
|
+
pixelId: null,
|
|
51
|
+
conversionEvent: "Purchase",
|
|
52
|
+
pages: [],
|
|
53
|
+
contact: { phone: null, email: null, address: null },
|
|
54
|
+
otpEnabled: false,
|
|
55
|
+
turnstileEnabled: false,
|
|
56
|
+
turnstileSiteKey: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/variant-picker.d.ts
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
* or not made with the other choices.
|
|
11
11
|
*/
|
|
12
12
|
import type { ProductVariant } from "./types.js";
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* What the picker reads of a variant — a page may hand it a lighter copy than
|
|
15
|
+
* `ProductVariant`. A product's variants are its active ones, in the
|
|
16
|
+
* merchant's order: the picker takes them as they come.
|
|
17
|
+
*/
|
|
18
|
+
export type PickerVariant = Pick<ProductVariant, "variations" | "inventory" | "isDefault">;
|
|
15
19
|
/** Option name → chosen value, as a product's `variantOptions` name them. */
|
|
16
20
|
export type OptionSelection = Readonly<Record<string, string>>;
|
|
17
21
|
export type OptionState = "available" | "sold_out" | "unavailable";
|
package/dist/variant-picker.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
const
|
|
2
|
-
const sellable = (product, variant) => live(variant) && (!product.trackInventory || variant.inventory > 0);
|
|
3
|
-
const byPosition = (a, b) => a.position - b.position;
|
|
1
|
+
const sellable = (product, variant) => !product.trackInventory || variant.inventory > 0;
|
|
4
2
|
/** The variant whose every option has the value `selection` gives it, or null. */
|
|
5
3
|
export function variantFor(variants, selection) {
|
|
6
|
-
return variants.find((variant) =>
|
|
4
|
+
return variants.find((variant) => Object.entries(variant.variations).every(([option, value]) => selection[option] === value)) ?? null;
|
|
7
5
|
}
|
|
8
6
|
/**
|
|
9
7
|
* The variant a product page opens on: the merchant's default when it can be
|
|
10
8
|
* sold, else the first that can, else the default (or first) shown sold out.
|
|
11
9
|
*/
|
|
12
10
|
export function initialVariant(product) {
|
|
13
|
-
const
|
|
14
|
-
const preferred = ordered.find((variant) => variant.isDefault) ?? ordered[0] ?? null;
|
|
11
|
+
const preferred = product.variants.find((variant) => variant.isDefault) ?? product.variants[0] ?? null;
|
|
15
12
|
if (preferred && sellable(product, preferred))
|
|
16
13
|
return preferred;
|
|
17
|
-
return
|
|
14
|
+
return product.variants.find((variant) => sellable(product, variant)) ?? preferred;
|
|
18
15
|
}
|
|
19
16
|
/**
|
|
20
17
|
* The shopper picks `value` for `option`. The variant they mean is the one
|
|
@@ -24,8 +21,8 @@ export function initialVariant(product) {
|
|
|
24
21
|
*/
|
|
25
22
|
export function pickOption(product, selection, option, value) {
|
|
26
23
|
const kept = (variant) => Object.entries(selection).filter(([name, chosen]) => variant.variations[name] === chosen).length;
|
|
27
|
-
const candidates = product.variants.filter((variant) =>
|
|
28
|
-
const best = candidates.sort((a, b) => kept(b) - kept(a) || Number(sellable(product, b)) - Number(sellable(product, a))
|
|
24
|
+
const candidates = product.variants.filter((variant) => variant.variations[option] === value);
|
|
25
|
+
const best = candidates.sort((a, b) => kept(b) - kept(a) || Number(sellable(product, b)) - Number(sellable(product, a)))[0];
|
|
29
26
|
return best ? { variant: best, selection: { ...best.variations } } : { variant: null, selection };
|
|
30
27
|
}
|
|
31
28
|
/**
|
package/dist/whatsapp.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* authors it within the limits below), and the page
|
|
7
7
|
* resolves it: `{store}`, `{product}`, `{url}`, `{order}`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* `whatsAppContact` is the one way a theme reads the store's WhatsApp: it
|
|
10
|
+
* picks the page's template, falls back to the theme's words, and applies
|
|
11
|
+
* three rules no theme re-invents:
|
|
10
12
|
*
|
|
11
13
|
* 1. **Ad-click parameters are stripped from `{url}`.** A shopper arriving from
|
|
12
14
|
* a paid ad should not hand the merchant's WhatsApp a `fbclid`-laden link.
|
|
@@ -55,9 +57,60 @@ export declare function resolvePrefillText({ template, storeName, productName, u
|
|
|
55
57
|
/** Appends the resolved message to a `wa.me` href, encoded and query-safe. */
|
|
56
58
|
export declare function buildWhatsAppHref(baseHref: string, text: string): string;
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* A theme's own words for the WhatsApp button, in the page's language: what
|
|
61
|
+
* each text reads when the merchant left it empty. A theme has them in every
|
|
62
|
+
* language it speaks; the store's texts come translated or not at all. The
|
|
63
|
+
* panel's three optional texts take null when the theme shows nothing there.
|
|
62
64
|
*/
|
|
63
|
-
export
|
|
65
|
+
export interface WhatsAppWords {
|
|
66
|
+
/** The floating launcher's text; null: the launcher shows its icon alone. */
|
|
67
|
+
launcher: string | null;
|
|
68
|
+
/** The line under the agent's name; null: none. */
|
|
69
|
+
caption: string | null;
|
|
70
|
+
/** The panel's greeting; null: none, and a launcher may open the chat straight away. */
|
|
71
|
+
welcome: string | null;
|
|
72
|
+
/** The button that opens WhatsApp from the panel, and any other WhatsApp link's text. */
|
|
73
|
+
cta: string;
|
|
74
|
+
/** The button on an order's confirmation or tracking page. */
|
|
75
|
+
orderButton: string;
|
|
76
|
+
/** The message each kind of page starts the chat with; each may carry `{store}`, `{product}`, `{url}` and `{order}`. */
|
|
77
|
+
messages: Record<WhatsAppPage["kind"], string>;
|
|
78
|
+
}
|
|
79
|
+
/** What a page is about, for the message the chat starts with. */
|
|
80
|
+
export interface WhatsAppPage {
|
|
81
|
+
/** A product or landing page names its product; an order's confirmation or tracking page its order. */
|
|
82
|
+
kind: "general" | "product" | "order";
|
|
83
|
+
/** The page's address, for `{url}`; its ad-click parameters are dropped. */
|
|
84
|
+
url: string;
|
|
85
|
+
product?: string | null | undefined;
|
|
86
|
+
order?: string | null | undefined;
|
|
87
|
+
}
|
|
88
|
+
/** The store's WhatsApp on one page, every text resolved — `whatsAppContact`. */
|
|
89
|
+
export interface WhatsAppContact {
|
|
90
|
+
/** The chat link, with this page's message in it. */
|
|
91
|
+
href: string;
|
|
92
|
+
launcherLabel: string | null;
|
|
93
|
+
caption: string | null;
|
|
94
|
+
welcomeMessage: string | null;
|
|
95
|
+
ctaLabel: string;
|
|
96
|
+
orderButtonLabel: string;
|
|
97
|
+
/** Who the shopper writes to: the merchant's agent, else the store. */
|
|
98
|
+
agentName: string;
|
|
99
|
+
avatarUrl: string | null;
|
|
100
|
+
accent: StoreWhatsAppWidget["accent"];
|
|
101
|
+
position: StoreWhatsAppWidget["position"];
|
|
102
|
+
attention: boolean;
|
|
103
|
+
/** The pages the floating launcher shows on (`showsWidgetOn`); a link elsewhere on the page does not depend on it. */
|
|
104
|
+
surfaces: StoreWhatsAppWidget["surfaces"];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The store's WhatsApp for one page, or null when it has none: every link a
|
|
108
|
+
* theme draws to it — the launcher, a footer or menu link, a contact card,
|
|
109
|
+
* the order confirmation's button — comes from here, so each carries the
|
|
110
|
+
* page's message and every text is in the page's language.
|
|
111
|
+
*
|
|
112
|
+
* Each text is the merchant's when they wrote one, else the theme's `words`.
|
|
113
|
+
* The message is the page's own template (a product's, an order's), else the
|
|
114
|
+
* merchant's general one, else the theme's for that kind of page.
|
|
115
|
+
*/
|
|
116
|
+
export declare function whatsAppContact(config: Pick<StoreConfig, "whatsapp" | "name">, page: WhatsAppPage, words: WhatsAppWords): WhatsAppContact | null;
|
package/dist/whatsapp.js
CHANGED
|
@@ -69,17 +69,35 @@ export function buildWhatsAppHref(baseHref, text) {
|
|
|
69
69
|
const separator = baseHref.includes("?") ? "&" : "?";
|
|
70
70
|
return `${baseHref}${separator}text=${encodeURIComponent(text)}`;
|
|
71
71
|
}
|
|
72
|
-
/** The widget's texts the merchant writes once, in the store's primary language. */
|
|
73
|
-
const WIDGET_TEXTS = ["caption", "welcomeMessage", "launcherLabel", "ctaLabel", "prefillGeneral", "prefillProduct", "thankYouButtonLabel", "prefillThankYou"];
|
|
74
72
|
/**
|
|
75
|
-
* The WhatsApp
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* The store's WhatsApp for one page, or null when it has none: every link a
|
|
74
|
+
* theme draws to it — the launcher, a footer or menu link, a contact card,
|
|
75
|
+
* the order confirmation's button — comes from here, so each carries the
|
|
76
|
+
* page's message and every text is in the page's language.
|
|
77
|
+
*
|
|
78
|
+
* Each text is the merchant's when they wrote one, else the theme's `words`.
|
|
79
|
+
* The message is the page's own template (a product's, an order's), else the
|
|
80
|
+
* merchant's general one, else the theme's for that kind of page.
|
|
79
81
|
*/
|
|
80
|
-
export function
|
|
82
|
+
export function whatsAppContact(config, page, words) {
|
|
81
83
|
const widget = config.whatsapp;
|
|
82
|
-
if (!widget
|
|
83
|
-
return
|
|
84
|
-
|
|
84
|
+
if (!widget)
|
|
85
|
+
return null;
|
|
86
|
+
const own = page.kind === "product" ? widget.prefillProduct : page.kind === "order" ? widget.prefillThankYou : null;
|
|
87
|
+
const template = own ?? widget.prefillGeneral ?? words.messages[page.kind];
|
|
88
|
+
const message = resolvePrefillText({ template, storeName: config.name, productName: page.product ?? null, url: page.url, orderNumber: page.order ?? null });
|
|
89
|
+
return {
|
|
90
|
+
href: buildWhatsAppHref(widget.href, message),
|
|
91
|
+
launcherLabel: widget.launcherLabel ?? words.launcher,
|
|
92
|
+
caption: widget.caption ?? words.caption,
|
|
93
|
+
welcomeMessage: widget.welcomeMessage ?? words.welcome,
|
|
94
|
+
ctaLabel: widget.ctaLabel ?? words.cta,
|
|
95
|
+
orderButtonLabel: widget.thankYouButtonLabel ?? words.orderButton,
|
|
96
|
+
agentName: widget.agentName ?? config.name,
|
|
97
|
+
avatarUrl: widget.avatarUrl,
|
|
98
|
+
accent: widget.accent,
|
|
99
|
+
position: widget.position,
|
|
100
|
+
attention: widget.attention,
|
|
101
|
+
surfaces: widget.surfaces,
|
|
102
|
+
};
|
|
85
103
|
}
|
package/package.json
CHANGED