@wocart/storefront 2.0.0 → 2.2.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 +36 -0
- package/USAGE.md +40 -4
- package/dist/client/pages.d.ts +10 -4
- package/dist/client/pages.js +7 -3
- package/dist/countdown.d.ts +28 -0
- package/dist/countdown.js +55 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +5 -0
- package/dist/schemas.js +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/money.d.ts +7 -7
- package/dist/utils/money.js +8 -12
- package/dist/whatsapp.d.ts +8 -0
- package/dist/whatsapp.js +14 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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
|
+
## 2.2.0 — 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `localWhatsAppWidget(config)`: the WhatsApp widget as a page in
|
|
13
|
+
`config.locale` shows it. Its texts are written once, in the store's
|
|
14
|
+
primary language; on a page in another language they are null, so a theme
|
|
15
|
+
shows its own words in the shopper's language instead of the merchant's in
|
|
16
|
+
another one. The agent's name and every setting stay.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `formatMoney` writes the currency symbol after the amount in every
|
|
21
|
+
language, as Algerian prices are written (`5 000 DA`, `5,000 DA`,
|
|
22
|
+
`5.000 دج`), joined by a no-break space so the two never wrap apart. French
|
|
23
|
+
and English put it before the amount (`DA 5 000`). Without a symbol it is
|
|
24
|
+
the amount alone, with no stray space.
|
|
25
|
+
|
|
26
|
+
## 2.1.0 — 2026-10-31
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `LandingPage.document`: the page's own composition for the client's
|
|
31
|
+
`theme`, or null — a landing page and its document in one request. Null
|
|
32
|
+
from a server that predates it, and a malformed document reads as null.
|
|
33
|
+
- `pages.landing(slug, { preview: true })` answers a draft page too and
|
|
34
|
+
counts no visit, for an editor's preview.
|
|
35
|
+
- `countdownTarget`, `countdownEnd` and `timeLeft`: an offer countdown's end
|
|
36
|
+
— every midnight, or a day the merchant names, at its end or at a time —
|
|
37
|
+
in Algiers time, the same in the server's render and the browser.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `pages.landing` sends the client's language, like every read that carries
|
|
42
|
+
merchant text: the page's product reaches a shopper in their language.
|
|
43
|
+
|
|
8
44
|
## 2.0.0 — 2026-10-30
|
|
9
45
|
|
|
10
46
|
A store's images can live on any media host. The server answers every image
|
package/USAGE.md
CHANGED
|
@@ -223,7 +223,7 @@ server directly.
|
|
|
223
223
|
| `isWilayaId` | Whether a value is a wilaya id — the range `WilayaIdSchema` checks on the server, without a schema in the browser |
|
|
224
224
|
| `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
225
|
| `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
|
|
226
|
-
| `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | WhatsApp prefill text and the `wa.me` link |
|
|
226
|
+
| `localWhatsAppWidget`, `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | the widget in the page's language, WhatsApp prefill text and the `wa.me` link |
|
|
227
227
|
| `storefrontPath`, `matchStorefrontPath`, `STOREFRONT_PAGE_PATHS` | The page types and their public paths: build a link, recognise a page |
|
|
228
228
|
| `showsWidgetOn`, `STOREFRONT_ROUTES` | Which widget surface a pathname is (`STOREFRONT_ROUTES` deprecated) |
|
|
229
229
|
| `productImageSource` | Which photo represents a product (cover → gallery fallback) |
|
|
@@ -998,7 +998,19 @@ const landing = await store.pages.landing("summer-sale");
|
|
|
998
998
|
|
|
999
999
|
A landing page can contain its own name, SEO metadata, image gap, images,
|
|
1000
1000
|
product, publishedAt, and tracking information. The product may be null.
|
|
1001
|
-
Draft, archived, or unknown landing pages resolve to null.
|
|
1001
|
+
Draft, archived, or unknown landing pages resolve to null. Its product
|
|
1002
|
+
arrives in the client's language.
|
|
1003
|
+
|
|
1004
|
+
A client created with `theme` also receives the page's own composition,
|
|
1005
|
+
`landing.document` (the page document `lp:<id>`), or null when it has none:
|
|
1006
|
+
render it with `@wocart/theme` against the theme's `landing` surface, which
|
|
1007
|
+
gives every landing page its starting document. An editor's preview reads a
|
|
1008
|
+
draft page too, and counts no visit, with `{ preview: true }` — never on a
|
|
1009
|
+
shopper's page:
|
|
1010
|
+
|
|
1011
|
+
~~~ts
|
|
1012
|
+
const landing = await store.pages.landing(slug, { preview: true });
|
|
1013
|
+
~~~
|
|
1002
1014
|
|
|
1003
1015
|
Landing tracking is already resolved by the server:
|
|
1004
1016
|
|
|
@@ -1010,6 +1022,21 @@ if (landing?.tracking.pixelId) {
|
|
|
1010
1022
|
|
|
1011
1023
|
Do not invent tracking events from a landing-page slug.
|
|
1012
1024
|
|
|
1025
|
+
### An offer's countdown
|
|
1026
|
+
|
|
1027
|
+
~~~ts
|
|
1028
|
+
import { countdownEnd, countdownTarget, timeLeft } from "@wocart/storefront";
|
|
1029
|
+
|
|
1030
|
+
const ends = countdownTarget(settings.mode, settings.endsOn, settings.endsAt); // "midnight", "2026-11-30", "2026-11-30T18:30"
|
|
1031
|
+
const end = countdownEnd(ends); // null: ended, or no real moment — show nothing
|
|
1032
|
+
const left = end === null ? null : timeLeft(end);
|
|
1033
|
+
~~~
|
|
1034
|
+
|
|
1035
|
+
A countdown ends every day at midnight, or on a day the merchant names, at
|
|
1036
|
+
its end or at the time they give, in Algiers time. The server writes the time
|
|
1037
|
+
left; the browser keeps it running with the same functions. A countdown that
|
|
1038
|
+
has ended shows nothing, never zeros.
|
|
1039
|
+
|
|
1013
1040
|
### The saved page document (theme composition)
|
|
1014
1041
|
|
|
1015
1042
|
~~~ts
|
|
@@ -1899,21 +1926,30 @@ already written, and the thank-you page offers the same handoff for an order.
|
|
|
1899
1926
|
The template belongs to the merchant — the dashboard authors and previews it
|
|
1900
1927
|
within the platform's limits — while the page resolves it:
|
|
1901
1928
|
|
|
1929
|
+
The merchant writes the widget's texts once, in the store's primary language.
|
|
1930
|
+
Read the widget with `localWhatsAppWidget(config)`: on a page in another
|
|
1931
|
+
language its texts (labels, welcome, prefills) are null, so the theme shows its
|
|
1932
|
+
own words in the shopper's language; the agent's name and the settings stay.
|
|
1933
|
+
|
|
1902
1934
|
~~~ts
|
|
1903
1935
|
import {
|
|
1904
1936
|
buildWhatsAppHref,
|
|
1937
|
+
localWhatsAppWidget,
|
|
1905
1938
|
resolvePrefillText,
|
|
1906
1939
|
} from "@wocart/storefront";
|
|
1907
1940
|
|
|
1941
|
+
const widget = localWhatsAppWidget(config);
|
|
1942
|
+
if (!widget) return;
|
|
1943
|
+
|
|
1908
1944
|
const text = resolvePrefillText({
|
|
1909
|
-
template:
|
|
1945
|
+
template: widget.prefillProduct ?? "Hello {store}, about {product}: {url}",
|
|
1910
1946
|
storeName: config.name,
|
|
1911
1947
|
productName: product.name, // falls back to the store name when absent
|
|
1912
1948
|
url: Astro.url.href, // ad-click parameters (`utm_*`, `fbclid`) stripped
|
|
1913
1949
|
orderNumber: null, // resolves to nothing when absent
|
|
1914
1950
|
});
|
|
1915
1951
|
|
|
1916
|
-
const href = buildWhatsAppHref(
|
|
1952
|
+
const href = buildWhatsAppHref(widget.href, text);
|
|
1917
1953
|
~~~
|
|
1918
1954
|
|
|
1919
1955
|
Three rules come with it, and a theme must not re-implement them:
|
package/dist/client/pages.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - `get(slug)` → null means "not published" — the theme 404s, never a
|
|
10
10
|
* soft 200 with empty content.
|
|
11
11
|
* - `landing(slug)` → null means draft/archived/unknown — the theme
|
|
12
|
-
* redirects to the store home instead of a dead end.
|
|
12
|
+
* redirects to the store home instead of a dead end. `{ preview: true }`
|
|
13
|
+
* also answers a draft page and counts no visit: an editor's preview.
|
|
13
14
|
*/
|
|
14
15
|
import { type ClientCore } from "./core.js";
|
|
15
16
|
import type { LandingPage, StorePagePublic } from "../types.js";
|
|
@@ -22,10 +23,15 @@ export interface PagesNamespace {
|
|
|
22
23
|
get(slug: string): Promise<StorePagePublic | null>;
|
|
23
24
|
/**
|
|
24
25
|
* Get a published landing page by its public slug: the ordered image
|
|
25
|
-
* stack, spacing settings,
|
|
26
|
-
* shape
|
|
26
|
+
* stack, spacing settings, the linked product in full store-product
|
|
27
|
+
* shape, and the page's own document for the client's `theme`. Resolves to
|
|
28
|
+
* `null` for draft/archived/unknown slugs or failure. With
|
|
29
|
+
* `{ preview: true }` a draft page answers too and no visit is counted —
|
|
30
|
+
* for an editor's preview only, never a shopper's page.
|
|
27
31
|
*/
|
|
28
|
-
landing(slug: string
|
|
32
|
+
landing(slug: string, options?: {
|
|
33
|
+
preview?: boolean;
|
|
34
|
+
}): Promise<LandingPage | null>;
|
|
29
35
|
}
|
|
30
36
|
/**
|
|
31
37
|
* Create pages namespace
|
package/dist/client/pages.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - `get(slug)` → null means "not published" — the theme 404s, never a
|
|
10
10
|
* soft 200 with empty content.
|
|
11
11
|
* - `landing(slug)` → null means draft/archived/unknown — the theme
|
|
12
|
-
* redirects to the store home instead of a dead end.
|
|
12
|
+
* redirects to the store home instead of a dead end. `{ preview: true }`
|
|
13
|
+
* also answers a draft page and counts no visit: an editor's preview.
|
|
13
14
|
*/
|
|
14
15
|
import { LandingPageSchema, StorePagePublicSchema } from "../schemas.js";
|
|
15
16
|
import { STOREFRONT_OPERATIONS } from "../operations.js";
|
|
@@ -31,9 +32,12 @@ export function createPagesNamespace(core) {
|
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
},
|
|
34
|
-
async landing(slug) {
|
|
35
|
+
async landing(slug, options = {}) {
|
|
35
36
|
try {
|
|
36
|
-
const landingPage = await core.get(`/store/landing-pages/${encodeURIComponent(slug)}`,
|
|
37
|
+
const landingPage = await core.get(localizedPath(core.config, `/store/landing-pages/${encodeURIComponent(slug)}`, {
|
|
38
|
+
theme: core.config.theme,
|
|
39
|
+
preview: options.preview === true ? "1" : undefined,
|
|
40
|
+
}), LandingPageSchema.nullable(), { operation: STOREFRONT_OPERATIONS.pagesLanding });
|
|
37
41
|
return landingPage ?? null;
|
|
38
42
|
}
|
|
39
43
|
catch (error) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An offer countdown's end, as the store sets it: every day at midnight, or
|
|
3
|
+
* a day the merchant names — at its end or at the time they give — in
|
|
4
|
+
* Algiers time (UTC+1 all year). An end already past, or a day or time that
|
|
5
|
+
* does not exist, is no countdown at all: a shopper never sees zeros. Runs
|
|
6
|
+
* the same on the server, which writes the time left, and in the browser,
|
|
7
|
+
* which keeps it running.
|
|
8
|
+
*/
|
|
9
|
+
/** What a countdown counts to: `midnight`, a day (`YYYY-MM-DD`) or a day at a time (`YYYY-MM-DDTHH:MM`). */
|
|
10
|
+
export type CountdownEnd = "midnight" | string;
|
|
11
|
+
/** The time left, in whole units. */
|
|
12
|
+
export interface TimeLeft {
|
|
13
|
+
days: number;
|
|
14
|
+
hours: number;
|
|
15
|
+
minutes: number;
|
|
16
|
+
seconds: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What a countdown's settings count to: `mode` `midnight` or `date`, the
|
|
20
|
+
* day the merchant typed (`YYYY-MM-DD`) and, optionally, the time
|
|
21
|
+
* (`HH:MM`). Undefined when they count to nothing — no day for `date`, or
|
|
22
|
+
* any other mode.
|
|
23
|
+
*/
|
|
24
|
+
export declare function countdownTarget(mode: string | undefined, endsOn: string | undefined, endsAt: string | undefined): CountdownEnd | undefined;
|
|
25
|
+
/** When a countdown ends, as an instant (ms since the epoch); null when it has ended or names no real moment. */
|
|
26
|
+
export declare function countdownEnd(ends: CountdownEnd | undefined, now?: number): number | null;
|
|
27
|
+
/** The time left until `end`, in whole days, hours, minutes and seconds; zero once it has passed. */
|
|
28
|
+
export declare function timeLeft(end: number, now?: number): TimeLeft;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An offer countdown's end, as the store sets it: every day at midnight, or
|
|
3
|
+
* a day the merchant names — at its end or at the time they give — in
|
|
4
|
+
* Algiers time (UTC+1 all year). An end already past, or a day or time that
|
|
5
|
+
* does not exist, is no countdown at all: a shopper never sees zeros. Runs
|
|
6
|
+
* the same on the server, which writes the time left, and in the browser,
|
|
7
|
+
* which keeps it running.
|
|
8
|
+
*/
|
|
9
|
+
const ALGERIA_OFFSET_MS = 60 * 60 * 1000;
|
|
10
|
+
const MOMENT = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?$/;
|
|
11
|
+
const TIME = /^(\d{1,2}):(\d{2})$/;
|
|
12
|
+
/**
|
|
13
|
+
* What a countdown's settings count to: `mode` `midnight` or `date`, the
|
|
14
|
+
* day the merchant typed (`YYYY-MM-DD`) and, optionally, the time
|
|
15
|
+
* (`HH:MM`). Undefined when they count to nothing — no day for `date`, or
|
|
16
|
+
* any other mode.
|
|
17
|
+
*/
|
|
18
|
+
export function countdownTarget(mode, endsOn, endsAt) {
|
|
19
|
+
if (mode === "midnight")
|
|
20
|
+
return "midnight";
|
|
21
|
+
const day = endsOn?.trim() ?? "";
|
|
22
|
+
if (mode !== "date" || day === "")
|
|
23
|
+
return undefined;
|
|
24
|
+
const time = TIME.exec(endsAt?.trim() ?? "");
|
|
25
|
+
return time ? `${day}T${time[1].padStart(2, "0")}:${time[2]}` : day;
|
|
26
|
+
}
|
|
27
|
+
/** When a countdown ends, as an instant (ms since the epoch); null when it has ended or names no real moment. */
|
|
28
|
+
export function countdownEnd(ends, now = Date.now()) {
|
|
29
|
+
if (ends === undefined || ends === "")
|
|
30
|
+
return null;
|
|
31
|
+
if (ends === "midnight") {
|
|
32
|
+
const local = new Date(now + ALGERIA_OFFSET_MS);
|
|
33
|
+
return Date.UTC(local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate() + 1) - ALGERIA_OFFSET_MS;
|
|
34
|
+
}
|
|
35
|
+
const match = MOMENT.exec(ends.trim());
|
|
36
|
+
if (!match)
|
|
37
|
+
return null;
|
|
38
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
39
|
+
const check = new Date(Date.UTC(year, month - 1, day));
|
|
40
|
+
if (check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day)
|
|
41
|
+
return null;
|
|
42
|
+
let end = Date.UTC(year, month - 1, day + 1) - ALGERIA_OFFSET_MS;
|
|
43
|
+
if (match[4] !== undefined) {
|
|
44
|
+
const [hours, minutes] = [Number(match[4]), Number(match[5])];
|
|
45
|
+
if (hours > 23 || minutes > 59)
|
|
46
|
+
return null;
|
|
47
|
+
end = Date.UTC(year, month - 1, day, hours, minutes) - ALGERIA_OFFSET_MS;
|
|
48
|
+
}
|
|
49
|
+
return end > now ? end : null;
|
|
50
|
+
}
|
|
51
|
+
/** The time left until `end`, in whole days, hours, minutes and seconds; zero once it has passed. */
|
|
52
|
+
export function timeLeft(end, now = Date.now()) {
|
|
53
|
+
const total = Math.max(0, Math.floor((end - now) / 1000));
|
|
54
|
+
return { days: Math.floor(total / 86400), hours: Math.floor((total % 86400) / 3600), minutes: Math.floor((total % 3600) / 60), seconds: total % 60 };
|
|
55
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ 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, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
|
|
18
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
|
|
19
19
|
export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
|
|
20
20
|
export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
|
|
21
21
|
export { servedLanguage } from "./locales.js";
|
|
@@ -49,6 +49,8 @@ export * from "./domain.js";
|
|
|
49
49
|
export * from "./operations.js";
|
|
50
50
|
export * from "./product-view.js";
|
|
51
51
|
export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
|
|
52
|
+
export { countdownEnd, countdownTarget, timeLeft } from "./countdown.js";
|
|
53
|
+
export type { CountdownEnd, TimeLeft } from "./countdown.js";
|
|
52
54
|
export { COUPON_HINT_REASONS, COUPON_NOT_APPLICABLE, COUPON_REFUSAL_REASONS, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf, couponRefusalTone, } from "./coupon.js";
|
|
53
55
|
export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
|
|
54
56
|
export type { OptionSelection, OptionState, PickerVariant } from "./variant-picker.js";
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
|
|
15
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, localWhatsAppWidget, } from "./whatsapp.js";
|
|
16
16
|
export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
|
|
17
17
|
export { servedLanguage } from "./locales.js";
|
|
18
18
|
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
@@ -36,6 +36,7 @@ export * from "./domain.js";
|
|
|
36
36
|
export * from "./operations.js";
|
|
37
37
|
export * from "./product-view.js";
|
|
38
38
|
export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
|
|
39
|
+
export { countdownEnd, countdownTarget, timeLeft } from "./countdown.js";
|
|
39
40
|
export { COUPON_HINT_REASONS, COUPON_NOT_APPLICABLE, COUPON_REFUSAL_REASONS, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf, couponRefusalTone, } from "./coupon.js";
|
|
40
41
|
export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
|
|
41
42
|
export { ABANDONED_CHECKOUT_SESSION_KEY, convertAbandonedCheckout, createAbandonedCheckoutTracker, createBrowserAbandonedCheckoutTracker, } from "./abandoned-checkout.js";
|
package/dist/schemas.d.ts
CHANGED
|
@@ -746,6 +746,11 @@ export declare const LandingPageSchema: z.ZodObject<{
|
|
|
746
746
|
rewardUnitPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
747
747
|
}, z.core.$strip>>;
|
|
748
748
|
}, z.core.$strip>>;
|
|
749
|
+
document: z.ZodDefault<z.ZodCatch<z.ZodNullable<z.ZodObject<{
|
|
750
|
+
version: z.ZodNumber;
|
|
751
|
+
surface: z.ZodString;
|
|
752
|
+
sections: z.ZodArray<z.ZodUnknown>;
|
|
753
|
+
}, z.core.$loose>>>>;
|
|
749
754
|
tracking: z.ZodObject<{
|
|
750
755
|
pixelId: z.ZodNullable<z.ZodString>;
|
|
751
756
|
conversionEvent: z.ZodEnum<{
|
package/dist/schemas.js
CHANGED
|
@@ -341,6 +341,7 @@ export const LandingPageSchema = z.object({
|
|
|
341
341
|
publishedAt: z.string().nullable(),
|
|
342
342
|
images: z.array(LandingPageImageSchema),
|
|
343
343
|
product: ProductSchema.nullable(),
|
|
344
|
+
document: PageDocumentPayloadSchema.nullable().catch(null).default(null),
|
|
344
345
|
tracking: z.object({
|
|
345
346
|
pixelId: z.string().nullable(),
|
|
346
347
|
conversionEvent: z.enum([
|
package/dist/types.d.ts
CHANGED
|
@@ -296,6 +296,12 @@ export interface LandingPage {
|
|
|
296
296
|
pixelId: string | null;
|
|
297
297
|
conversionEvent: "Lead" | "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered";
|
|
298
298
|
};
|
|
299
|
+
/**
|
|
300
|
+
* The page's own composition for the client's `theme` (its `lp:<id>`
|
|
301
|
+
* document), or null when it has none, the client names no theme, or the
|
|
302
|
+
* server predates it: the theme then renders its `landing` default.
|
|
303
|
+
*/
|
|
304
|
+
document: PageDocumentPayload | null;
|
|
299
305
|
}
|
|
300
306
|
export interface Wilaya {
|
|
301
307
|
id: number;
|
package/dist/utils/money.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deep Money Formatter
|
|
3
3
|
*
|
|
4
4
|
* One formatter for every price a storefront shows.
|
|
5
|
-
* Handles locale-aware formatting
|
|
5
|
+
* Handles locale-aware formatting and Arabic-Indic digits; the symbol follows the amount.
|
|
6
6
|
*
|
|
7
7
|
* Design principle: ONE function that handles all cases. Callers curry if needed.
|
|
8
8
|
*/
|
|
@@ -26,14 +26,14 @@ export interface MoneyFormatOptions {
|
|
|
26
26
|
* // Arabic: 5,000 دج (with Arabic-Indic digits)
|
|
27
27
|
* formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
|
|
28
28
|
*
|
|
29
|
-
* // French:
|
|
30
|
-
* formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
|
|
29
|
+
* // French: 5 000,00 DA
|
|
30
|
+
* formatMoney(5000, { symbol: 'DA', locale: 'fr-FR', decimals: 2 })
|
|
31
31
|
*
|
|
32
|
-
* // English:
|
|
32
|
+
* // English: 5,000 DA
|
|
33
33
|
* formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
|
|
34
|
-
*
|
|
35
|
-
* // No decimals: 5,000 دج
|
|
36
|
-
* formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
|
|
37
34
|
* ```
|
|
35
|
+
*
|
|
36
|
+
* The symbol follows the amount in every language, as Algerian prices are
|
|
37
|
+
* written, joined by a no-break space so the two never wrap apart.
|
|
38
38
|
*/
|
|
39
39
|
export declare function formatMoney(amount: number, options: MoneyFormatOptions): string;
|
package/dist/utils/money.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deep Money Formatter
|
|
3
3
|
*
|
|
4
4
|
* One formatter for every price a storefront shows.
|
|
5
|
-
* Handles locale-aware formatting
|
|
5
|
+
* Handles locale-aware formatting and Arabic-Indic digits; the symbol follows the amount.
|
|
6
6
|
*
|
|
7
7
|
* Design principle: ONE function that handles all cases. Callers curry if needed.
|
|
8
8
|
*/
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
* // Arabic: 5,000 دج (with Arabic-Indic digits)
|
|
19
19
|
* formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
|
|
20
20
|
*
|
|
21
|
-
* // French:
|
|
22
|
-
* formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
|
|
21
|
+
* // French: 5 000,00 DA
|
|
22
|
+
* formatMoney(5000, { symbol: 'DA', locale: 'fr-FR', decimals: 2 })
|
|
23
23
|
*
|
|
24
|
-
* // English:
|
|
24
|
+
* // English: 5,000 DA
|
|
25
25
|
* formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
|
|
26
|
-
*
|
|
27
|
-
* // No decimals: 5,000 دج
|
|
28
|
-
* formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
|
|
29
26
|
* ```
|
|
27
|
+
*
|
|
28
|
+
* The symbol follows the amount in every language, as Algerian prices are
|
|
29
|
+
* written, joined by a no-break space so the two never wrap apart.
|
|
30
30
|
*/
|
|
31
31
|
export function formatMoney(amount, options) {
|
|
32
32
|
const { symbol, locale, decimals = 0 } = options;
|
|
@@ -35,9 +35,5 @@ export function formatMoney(amount, options) {
|
|
|
35
35
|
minimumFractionDigits: decimals,
|
|
36
36
|
maximumFractionDigits: decimals,
|
|
37
37
|
});
|
|
38
|
-
|
|
39
|
-
// Arabic: suffix (5,000.00 دج)
|
|
40
|
-
// French/English: prefix (DA 5,000.00)
|
|
41
|
-
const isRTL = locale.startsWith("ar");
|
|
42
|
-
return isRTL ? `${formatted} ${symbol}` : `${symbol} ${formatted}`;
|
|
38
|
+
return symbol ? `${formatted}\u00A0${symbol}` : formatted;
|
|
43
39
|
}
|
package/dist/whatsapp.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 3. **The encoded text is appended the same way everywhere**, so a base href
|
|
17
17
|
* that already carries a query string keeps it.
|
|
18
18
|
*/
|
|
19
|
+
import type { StoreConfig, StoreWhatsAppWidget } from "./types.js";
|
|
19
20
|
/**
|
|
20
21
|
* The hard cap on a resolved prefill message, in characters.
|
|
21
22
|
*
|
|
@@ -53,3 +54,10 @@ export interface ResolvePrefillParams {
|
|
|
53
54
|
export declare function resolvePrefillText({ template, storeName, productName, url, orderNumber, }: ResolvePrefillParams): string;
|
|
54
55
|
/** Appends the resolved message to a `wa.me` href, encoded and query-safe. */
|
|
55
56
|
export declare function buildWhatsAppHref(baseHref: string, text: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* The WhatsApp widget as a page in `config.locale` shows it. The merchant
|
|
59
|
+
* writes its texts once, in the store's primary language (`config.lang`), so
|
|
60
|
+
* a page in another language gets them as null and shows the theme's own
|
|
61
|
+
* words in its language instead; the agent's name and every setting stay.
|
|
62
|
+
*/
|
|
63
|
+
export declare function localWhatsAppWidget(config: Pick<StoreConfig, "whatsapp" | "lang" | "locale">): StoreWhatsAppWidget | null;
|
package/dist/whatsapp.js
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WhatsApp prefill messages — the storefront's message rule.
|
|
3
|
-
*
|
|
4
|
-
* A storefront's WhatsApp launcher hands the shopper to WhatsApp with the
|
|
5
|
-
* message already written. The template belongs to the merchant (the dashboard
|
|
6
|
-
* authors it within the limits below), and the page
|
|
7
|
-
* resolves it: `{store}`, `{product}`, `{url}`, `{order}`.
|
|
8
|
-
*
|
|
9
|
-
* Three rules live here so no theme re-invents them:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Ad-click parameters are stripped from `{url}`.** A shopper arriving from
|
|
12
|
-
* a paid ad should not hand the merchant's WhatsApp a `fbclid`-laden link.
|
|
13
|
-
* 2. **The resolved message is capped at `MAX_RESOLVED_PREFILL_LENGTH`, cut at
|
|
14
|
-
* a word boundary.** Browsers and WhatsApp both truncate silently past a
|
|
15
|
-
* point; a cut mid-word reads as a broken store.
|
|
16
|
-
* 3. **The encoded text is appended the same way everywhere**, so a base href
|
|
17
|
-
* that already carries a query string keeps it.
|
|
18
|
-
*/
|
|
19
1
|
/**
|
|
20
2
|
* The hard cap on a resolved prefill message, in characters.
|
|
21
3
|
*
|
|
@@ -87,3 +69,17 @@ export function buildWhatsAppHref(baseHref, text) {
|
|
|
87
69
|
const separator = baseHref.includes("?") ? "&" : "?";
|
|
88
70
|
return `${baseHref}${separator}text=${encodeURIComponent(text)}`;
|
|
89
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
|
+
/**
|
|
75
|
+
* The WhatsApp widget as a page in `config.locale` shows it. The merchant
|
|
76
|
+
* writes its texts once, in the store's primary language (`config.lang`), so
|
|
77
|
+
* a page in another language gets them as null and shows the theme's own
|
|
78
|
+
* words in its language instead; the agent's name and every setting stay.
|
|
79
|
+
*/
|
|
80
|
+
export function localWhatsAppWidget(config) {
|
|
81
|
+
const widget = config.whatsapp;
|
|
82
|
+
if (!widget || config.locale === config.lang)
|
|
83
|
+
return widget;
|
|
84
|
+
return { ...widget, ...Object.fromEntries(WIDGET_TEXTS.map((key) => [key, null])) };
|
|
85
|
+
}
|
package/package.json
CHANGED