@wocart/storefront 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/USAGE.md +150 -35
- package/dist/client/core.d.ts +6 -0
- package/dist/client/core.js +24 -4
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +3 -1
- package/dist/client/menus.d.ts +22 -0
- package/dist/client/menus.js +30 -0
- package/dist/client/products.d.ts +19 -7
- package/dist/client/products.js +22 -2
- package/dist/domain.d.ts +6 -0
- package/dist/domain.js +6 -0
- package/dist/endpoints/proxy.d.ts +8 -2
- package/dist/endpoints/proxy.js +5 -2
- package/dist/endpoints/storefront-routes.d.ts +13 -4
- package/dist/endpoints/storefront-routes.js +46 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/review-media.d.ts +87 -0
- package/dist/review-media.js +182 -0
- package/dist/routes.d.ts +60 -28
- package/dist/routes.js +80 -51
- package/dist/schemas.d.ts +91 -3
- package/dist/schemas.js +65 -1
- package/dist/types.d.ts +115 -3
- package/package.json +5 -1
package/dist/routes.js
CHANGED
|
@@ -1,59 +1,88 @@
|
|
|
1
|
+
/** Each page type's public path; `:handle` / `:slug` mark its one parameter. */
|
|
2
|
+
export const STOREFRONT_PAGE_PATHS = {
|
|
3
|
+
home: "/",
|
|
4
|
+
catalog: "/products",
|
|
5
|
+
category: "/category/:slug",
|
|
6
|
+
product: "/products/:handle",
|
|
7
|
+
page: "/pages/:slug",
|
|
8
|
+
landing: "/lp/:slug",
|
|
9
|
+
checkout: "/checkout",
|
|
10
|
+
thankYou: "/thank-you",
|
|
11
|
+
};
|
|
1
12
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
13
|
+
* The public path of a page type, its parameter encoded:
|
|
14
|
+
* `storefrontPath("product", { handle: "robe" })` → `/products/robe`.
|
|
15
|
+
* Throws when the parameter is missing — a link to nowhere is a bug to see,
|
|
16
|
+
* not a path to render.
|
|
5
17
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
export function storefrontPath(type, params) {
|
|
19
|
+
return STOREFRONT_PAGE_PATHS[type].replace(/:([a-z]+)/g, (_, name) => {
|
|
20
|
+
const value = params?.[name];
|
|
21
|
+
if (!value)
|
|
22
|
+
throw new Error(`storefrontPath("${type}") needs "${name}"`);
|
|
23
|
+
return encodeURIComponent(value);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const MATCHERS = Object.entries(STOREFRONT_PAGE_PATHS).map(([type, path]) => {
|
|
27
|
+
const names = [];
|
|
28
|
+
const source = path.replace(/:([a-z]+)/g, (_, name) => {
|
|
29
|
+
names.push(name);
|
|
30
|
+
return "([^/]+)";
|
|
31
|
+
});
|
|
32
|
+
return { type, names, pattern: new RegExp(`^${source}$`) };
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Which page type a pathname is, and its parameter (decoded) — or null for
|
|
36
|
+
* any other path. Exact matches only: `/products/` is the catalog,
|
|
37
|
+
* `/productsomething` and `/products/a/b` are nothing. The query, the
|
|
38
|
+
* fragment and a trailing slash are ignored. Pass a pathname without its
|
|
39
|
+
* language prefix (`resolveLocalePath` gives it).
|
|
40
|
+
*/
|
|
41
|
+
export function matchStorefrontPath(rawPathname) {
|
|
42
|
+
const pathname = rawPathname.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
|
43
|
+
for (const { type, names, pattern } of MATCHERS) {
|
|
44
|
+
const match = pattern.exec(pathname);
|
|
45
|
+
if (!match)
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
return { type, params: Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match[i + 1])])) };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** The WhatsApp widget surface each page type belongs to (the home surface is the root). */
|
|
57
|
+
const WIDGET_SURFACE = {
|
|
58
|
+
home: "home",
|
|
59
|
+
catalog: "catalog",
|
|
60
|
+
category: "catalog",
|
|
61
|
+
product: "product",
|
|
62
|
+
page: "pages",
|
|
63
|
+
landing: "landing",
|
|
64
|
+
checkout: "checkout",
|
|
65
|
+
thankYou: "thankYou",
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated Use `STOREFRONT_PAGE_PATHS` (every page type, the home and the
|
|
69
|
+
* catalog index included) with `storefrontPath` and `matchStorefrontPath`.
|
|
70
|
+
* Kept equal to what it always listed.
|
|
71
|
+
*/
|
|
72
|
+
export const STOREFRONT_ROUTES = ["product", "category", "page", "landing", "checkout", "thankYou"].map((type) => ({
|
|
73
|
+
path: STOREFRONT_PAGE_PATHS[type],
|
|
74
|
+
surface: WIDGET_SURFACE[type],
|
|
75
|
+
}));
|
|
14
76
|
/**
|
|
15
|
-
* Whether the widget belongs on a pathname
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* - `/` → the home surface;
|
|
22
|
-
* - `/products` (the catalog index) and `/category/:slug` → the catalog surface;
|
|
23
|
-
* - `/products/:handle` → the product surface;
|
|
24
|
-
* - `/pages/:slug`, `/lp/:slug`, `/checkout`, `/thank-you` → their own surfaces;
|
|
25
|
-
* - anything else — a 404, `/productsomething`, `/checkout-success` — → no
|
|
26
|
-
* surface at all, never a prefix match that leaks a widget onto a page the
|
|
27
|
-
* merchant switched off.
|
|
77
|
+
* Whether the widget belongs on a pathname: the merchant answers "which
|
|
78
|
+
* surfaces" (`surfaces[surface]`); this answers which surface a pathname *is*,
|
|
79
|
+
* from the page types above. Any other path — a 404, `/productsomething`,
|
|
80
|
+
* `/checkout-success` — is no surface at all, never a prefix match that leaks
|
|
81
|
+
* the widget onto a page the merchant switched off.
|
|
28
82
|
*/
|
|
29
83
|
export function showsWidgetOn(surfaces, rawPathname) {
|
|
30
84
|
if (!surfaces)
|
|
31
85
|
return false;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (pathname === "/") {
|
|
35
|
-
return Boolean(surfaces.home);
|
|
36
|
-
}
|
|
37
|
-
if (pathname === "/products" || pathname.startsWith("/category/")) {
|
|
38
|
-
return Boolean(surfaces.catalog);
|
|
39
|
-
}
|
|
40
|
-
if (pathname.startsWith("/products/")) {
|
|
41
|
-
const slug = pathname.slice("/products/".length);
|
|
42
|
-
return slug.length > 0 ? Boolean(surfaces.product) : Boolean(surfaces.catalog);
|
|
43
|
-
}
|
|
44
|
-
if (pathname === "/checkout") {
|
|
45
|
-
return Boolean(surfaces.checkout);
|
|
46
|
-
}
|
|
47
|
-
if (pathname === "/thank-you") {
|
|
48
|
-
return Boolean(surfaces.thankYou);
|
|
49
|
-
}
|
|
50
|
-
if (pathname.startsWith("/pages/")) {
|
|
51
|
-
const slug = pathname.slice("/pages/".length);
|
|
52
|
-
return slug.length > 0 ? Boolean(surfaces.pages) : false;
|
|
53
|
-
}
|
|
54
|
-
if (pathname.startsWith("/lp/")) {
|
|
55
|
-
const slug = pathname.slice("/lp/".length);
|
|
56
|
-
return slug.length > 0 ? Boolean(surfaces.landing) : false;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
86
|
+
const match = matchStorefrontPath(rawPathname);
|
|
87
|
+
return match ? Boolean(surfaces[WIDGET_SURFACE[match.type]]) : false;
|
|
59
88
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export declare const ProductListSchema: z.ZodObject<{
|
|
|
89
89
|
}>;
|
|
90
90
|
showInStore: z.ZodBoolean;
|
|
91
91
|
storeFeatured: z.ZodBoolean;
|
|
92
|
-
deletedAt: z.
|
|
92
|
+
deletedAt: z.ZodOptional<z.ZodNull>;
|
|
93
93
|
publishedAt: z.ZodNullable<z.ZodString>;
|
|
94
94
|
createdAt: z.ZodString;
|
|
95
95
|
updatedAt: z.ZodString;
|
|
@@ -142,7 +142,7 @@ export declare const ProductSchema: z.ZodObject<{
|
|
|
142
142
|
}>;
|
|
143
143
|
showInStore: z.ZodBoolean;
|
|
144
144
|
storeFeatured: z.ZodBoolean;
|
|
145
|
-
deletedAt: z.
|
|
145
|
+
deletedAt: z.ZodOptional<z.ZodNull>;
|
|
146
146
|
publishedAt: z.ZodNullable<z.ZodString>;
|
|
147
147
|
createdAt: z.ZodString;
|
|
148
148
|
updatedAt: z.ZodString;
|
|
@@ -234,6 +234,23 @@ export declare const StorePagePublicSchema: z.ZodObject<{
|
|
|
234
234
|
metaTitle: z.ZodNullable<z.ZodString>;
|
|
235
235
|
metaDescription: z.ZodNullable<z.ZodString>;
|
|
236
236
|
}, z.core.$strip>;
|
|
237
|
+
/**
|
|
238
|
+
* A navigation menu as a shopper sees it — GET /store/menus/{handle}.
|
|
239
|
+
* `type` is an open string: the server owns which destinations exist, so a
|
|
240
|
+
* new one never fails this parse.
|
|
241
|
+
*/
|
|
242
|
+
export declare const StoreMenuItemSchema: z.ZodType<StoreMenuItemShape>;
|
|
243
|
+
interface StoreMenuItemShape {
|
|
244
|
+
label: string;
|
|
245
|
+
href: string;
|
|
246
|
+
type: string;
|
|
247
|
+
children: StoreMenuItemShape[];
|
|
248
|
+
}
|
|
249
|
+
export declare const StoreMenuSchema: z.ZodObject<{
|
|
250
|
+
handle: z.ZodString;
|
|
251
|
+
title: z.ZodString;
|
|
252
|
+
items: z.ZodArray<z.ZodType<StoreMenuItemShape, unknown, z.core.$ZodTypeInternals<StoreMenuItemShape, unknown>>>;
|
|
253
|
+
}, z.core.$strip>;
|
|
237
254
|
/**
|
|
238
255
|
* The persisted theme page document (PageData): the
|
|
239
256
|
* TRANSPORT shape only. `sections` stays `unknown[]` so instances of a newer
|
|
@@ -373,6 +390,7 @@ export declare const StoreConfigSchema: z.ZodObject<{
|
|
|
373
390
|
}>;
|
|
374
391
|
}, z.core.$strip>>;
|
|
375
392
|
contentOverrides: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
393
|
+
themeSettings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
376
394
|
currency: z.ZodString;
|
|
377
395
|
currencySymbol: z.ZodString;
|
|
378
396
|
metaTitle: z.ZodNullable<z.ZodString>;
|
|
@@ -546,7 +564,7 @@ export declare const LandingPageSchema: z.ZodObject<{
|
|
|
546
564
|
}>;
|
|
547
565
|
showInStore: z.ZodBoolean;
|
|
548
566
|
storeFeatured: z.ZodBoolean;
|
|
549
|
-
deletedAt: z.
|
|
567
|
+
deletedAt: z.ZodOptional<z.ZodNull>;
|
|
550
568
|
publishedAt: z.ZodNullable<z.ZodString>;
|
|
551
569
|
createdAt: z.ZodString;
|
|
552
570
|
updatedAt: z.ZodString;
|
|
@@ -740,6 +758,18 @@ export declare const OtpVerifyResultSchema: z.ZodObject<{
|
|
|
740
758
|
status: z.ZodLiteral<"verified">;
|
|
741
759
|
otpToken: z.ZodString;
|
|
742
760
|
}, z.core.$strip>;
|
|
761
|
+
export declare const ReviewMediaSchema: z.ZodObject<{
|
|
762
|
+
id: z.ZodString;
|
|
763
|
+
kind: z.ZodEnum<{
|
|
764
|
+
audio: "audio";
|
|
765
|
+
image: "image";
|
|
766
|
+
}>;
|
|
767
|
+
url: z.ZodString;
|
|
768
|
+
thumbUrl: z.ZodNullable<z.ZodString>;
|
|
769
|
+
width: z.ZodNullable<z.ZodNumber>;
|
|
770
|
+
height: z.ZodNullable<z.ZodNumber>;
|
|
771
|
+
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
772
|
+
}, z.core.$strip>;
|
|
743
773
|
export declare const ReviewSchema: z.ZodPreprocess<z.ZodObject<{
|
|
744
774
|
id: z.ZodString;
|
|
745
775
|
customerName: z.ZodString;
|
|
@@ -747,7 +777,51 @@ export declare const ReviewSchema: z.ZodPreprocess<z.ZodObject<{
|
|
|
747
777
|
title: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
748
778
|
body: z.ZodString;
|
|
749
779
|
createdAt: z.ZodString;
|
|
780
|
+
verified: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
781
|
+
media: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
782
|
+
id: z.ZodString;
|
|
783
|
+
kind: z.ZodEnum<{
|
|
784
|
+
audio: "audio";
|
|
785
|
+
image: "image";
|
|
786
|
+
}>;
|
|
787
|
+
url: z.ZodString;
|
|
788
|
+
thumbUrl: z.ZodNullable<z.ZodString>;
|
|
789
|
+
width: z.ZodNullable<z.ZodNumber>;
|
|
790
|
+
height: z.ZodNullable<z.ZodNumber>;
|
|
791
|
+
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
792
|
+
}, z.core.$strip>>>>;
|
|
750
793
|
}, z.core.$strip>, unknown>;
|
|
794
|
+
export declare const ReviewEligibilitySchema: z.ZodObject<{
|
|
795
|
+
orderNumber: z.ZodString;
|
|
796
|
+
customerName: z.ZodString;
|
|
797
|
+
products: z.ZodArray<z.ZodObject<{
|
|
798
|
+
productId: z.ZodString;
|
|
799
|
+
name: z.ZodString;
|
|
800
|
+
imageSrc: z.ZodNullable<z.ZodString>;
|
|
801
|
+
reviewed: z.ZodBoolean;
|
|
802
|
+
}, z.core.$strip>>;
|
|
803
|
+
media: z.ZodNullable<z.ZodObject<{
|
|
804
|
+
maxPhotos: z.ZodNumber;
|
|
805
|
+
voiceMaxSeconds: z.ZodNullable<z.ZodNumber>;
|
|
806
|
+
grant: z.ZodString;
|
|
807
|
+
expiresAt: z.ZodNumber;
|
|
808
|
+
}, z.core.$strip>>;
|
|
809
|
+
}, z.core.$strip>;
|
|
810
|
+
export declare const ReviewUploadSchema: z.ZodObject<{
|
|
811
|
+
uploadId: z.ZodString;
|
|
812
|
+
kind: z.ZodEnum<{
|
|
813
|
+
audio: "audio";
|
|
814
|
+
image: "image";
|
|
815
|
+
}>;
|
|
816
|
+
bytes: z.ZodNumber;
|
|
817
|
+
}, z.core.$strip>;
|
|
818
|
+
export declare const ReviewSubmitResultSchema: z.ZodObject<{
|
|
819
|
+
id: z.ZodString;
|
|
820
|
+
visibility: z.ZodOptional<z.ZodEnum<{
|
|
821
|
+
after_review: "after_review";
|
|
822
|
+
published: "published";
|
|
823
|
+
}>>;
|
|
824
|
+
}, z.core.$strip>;
|
|
751
825
|
/**
|
|
752
826
|
* GET /store/reviews answers `{ success, data: rows, count, total }` — the
|
|
753
827
|
* total lives OUTSIDE `data`, so a caller that reads only `data` loses it.
|
|
@@ -760,6 +834,20 @@ export declare const ReviewListEnvelopeSchema: z.ZodObject<{
|
|
|
760
834
|
title: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
761
835
|
body: z.ZodString;
|
|
762
836
|
createdAt: z.ZodString;
|
|
837
|
+
verified: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
838
|
+
media: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
839
|
+
id: z.ZodString;
|
|
840
|
+
kind: z.ZodEnum<{
|
|
841
|
+
audio: "audio";
|
|
842
|
+
image: "image";
|
|
843
|
+
}>;
|
|
844
|
+
url: z.ZodString;
|
|
845
|
+
thumbUrl: z.ZodNullable<z.ZodString>;
|
|
846
|
+
width: z.ZodNullable<z.ZodNumber>;
|
|
847
|
+
height: z.ZodNullable<z.ZodNumber>;
|
|
848
|
+
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
849
|
+
}, z.core.$strip>>>>;
|
|
763
850
|
}, z.core.$strip>, unknown>>;
|
|
764
851
|
total: z.ZodNumber;
|
|
765
852
|
}, z.core.$strip>;
|
|
853
|
+
export {};
|
package/dist/schemas.js
CHANGED
|
@@ -83,7 +83,12 @@ const productBaseFields = {
|
|
|
83
83
|
status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
|
|
84
84
|
showInStore: z.boolean(),
|
|
85
85
|
storeFeatured: z.boolean(),
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @deprecated Always null: the server never serves a removed product. The
|
|
88
|
+
* server still sends it for older releases; it will stop, and a later major
|
|
89
|
+
* drops it. Do not read it.
|
|
90
|
+
*/
|
|
91
|
+
deletedAt: z.null().optional(),
|
|
87
92
|
publishedAt: z.string().datetime().nullable(),
|
|
88
93
|
createdAt: z.string().datetime(),
|
|
89
94
|
updatedAt: z.string().datetime(),
|
|
@@ -128,6 +133,22 @@ export const StorePagePublicSchema = z.object({
|
|
|
128
133
|
metaTitle: z.string().nullable(),
|
|
129
134
|
metaDescription: z.string().nullable(),
|
|
130
135
|
});
|
|
136
|
+
/**
|
|
137
|
+
* A navigation menu as a shopper sees it — GET /store/menus/{handle}.
|
|
138
|
+
* `type` is an open string: the server owns which destinations exist, so a
|
|
139
|
+
* new one never fails this parse.
|
|
140
|
+
*/
|
|
141
|
+
export const StoreMenuItemSchema = z.lazy(() => z.object({
|
|
142
|
+
label: z.string(),
|
|
143
|
+
href: z.string(),
|
|
144
|
+
type: z.string(),
|
|
145
|
+
children: z.array(StoreMenuItemSchema),
|
|
146
|
+
}));
|
|
147
|
+
export const StoreMenuSchema = z.object({
|
|
148
|
+
handle: z.string(),
|
|
149
|
+
title: z.string(),
|
|
150
|
+
items: z.array(StoreMenuItemSchema),
|
|
151
|
+
});
|
|
131
152
|
/**
|
|
132
153
|
* The persisted theme page document (PageData): the
|
|
133
154
|
* TRANSPORT shape only. `sections` stays `unknown[]` so instances of a newer
|
|
@@ -220,6 +241,11 @@ export const StoreConfigSchema = z.object({
|
|
|
220
241
|
locales: z.array(StorefrontLanguageSchema),
|
|
221
242
|
/** The requesting theme's text edits, one map per published language. */
|
|
222
243
|
contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
|
|
244
|
+
/**
|
|
245
|
+
* The store's values for the requesting theme's settings, as saved. Absent
|
|
246
|
+
* from a server older than theme settings.
|
|
247
|
+
*/
|
|
248
|
+
themeSettings: z.record(z.string(), z.unknown()).optional(),
|
|
223
249
|
currency: z.string(),
|
|
224
250
|
currencySymbol: z.string(),
|
|
225
251
|
metaTitle: z.string().nullable(),
|
|
@@ -359,6 +385,15 @@ export const OtpVerifyResultSchema = z.object({
|
|
|
359
385
|
status: z.literal("verified"),
|
|
360
386
|
otpToken: z.string(),
|
|
361
387
|
});
|
|
388
|
+
export const ReviewMediaSchema = z.object({
|
|
389
|
+
id: z.string(),
|
|
390
|
+
kind: z.enum(["image", "audio"]),
|
|
391
|
+
url: z.string(),
|
|
392
|
+
thumbUrl: z.string().nullable(),
|
|
393
|
+
width: z.number().int().nullable(),
|
|
394
|
+
height: z.number().int().nullable(),
|
|
395
|
+
durationMs: z.number().int().nullable(),
|
|
396
|
+
});
|
|
362
397
|
export const ReviewSchema = z.preprocess((raw) => {
|
|
363
398
|
if (typeof raw === "object" && raw !== null) {
|
|
364
399
|
const obj = raw;
|
|
@@ -375,7 +410,36 @@ export const ReviewSchema = z.preprocess((raw) => {
|
|
|
375
410
|
title: z.string().nullable().optional().default(null),
|
|
376
411
|
body: z.string(),
|
|
377
412
|
createdAt: z.string(),
|
|
413
|
+
verified: z.boolean().optional().default(false),
|
|
414
|
+
media: z.array(ReviewMediaSchema).optional().default([]),
|
|
378
415
|
}));
|
|
416
|
+
export const ReviewEligibilitySchema = z.object({
|
|
417
|
+
orderNumber: z.string(),
|
|
418
|
+
customerName: z.string(),
|
|
419
|
+
products: z.array(z.object({
|
|
420
|
+
productId: z.string(),
|
|
421
|
+
name: z.string(),
|
|
422
|
+
imageSrc: z.string().nullable(),
|
|
423
|
+
reviewed: z.boolean(),
|
|
424
|
+
})),
|
|
425
|
+
media: z
|
|
426
|
+
.object({
|
|
427
|
+
maxPhotos: z.number().int().min(0),
|
|
428
|
+
voiceMaxSeconds: z.number().int().positive().nullable(),
|
|
429
|
+
grant: z.string().min(1),
|
|
430
|
+
expiresAt: z.number().int(),
|
|
431
|
+
})
|
|
432
|
+
.nullable(),
|
|
433
|
+
});
|
|
434
|
+
export const ReviewUploadSchema = z.object({
|
|
435
|
+
uploadId: z.string(),
|
|
436
|
+
kind: z.enum(["image", "audio"]),
|
|
437
|
+
bytes: z.number().int(),
|
|
438
|
+
});
|
|
439
|
+
export const ReviewSubmitResultSchema = z.object({
|
|
440
|
+
id: z.string(),
|
|
441
|
+
visibility: z.enum(["published", "after_review"]).optional(),
|
|
442
|
+
});
|
|
379
443
|
/**
|
|
380
444
|
* GET /store/reviews answers `{ success, data: rows, count, total }` — the
|
|
381
445
|
* total lives OUTSIDE `data`, so a caller that reads only `data` loses it.
|
package/dist/types.d.ts
CHANGED
|
@@ -85,6 +85,15 @@ export interface StoreConfig {
|
|
|
85
85
|
* language — never another language's map.
|
|
86
86
|
*/
|
|
87
87
|
contentOverrides: Record<string, Record<string, string>>;
|
|
88
|
+
/**
|
|
89
|
+
* The merchant's values for this theme's settings, by setting key, exactly
|
|
90
|
+
* as saved — `{}` when there are none or the client was created without
|
|
91
|
+
* `theme`, absent from a server that predates theme settings. Read them
|
|
92
|
+
* against the theme's declaration with `resolveThemeSettings` from
|
|
93
|
+
* `@wocart/theme` (which takes either): a value the theme no longer accepts
|
|
94
|
+
* falls back to its default there.
|
|
95
|
+
*/
|
|
96
|
+
themeSettings?: Record<string, unknown> | undefined;
|
|
88
97
|
currency: string;
|
|
89
98
|
currencySymbol: string;
|
|
90
99
|
metaTitle: string | null;
|
|
@@ -178,6 +187,25 @@ export interface StorePagePublic {
|
|
|
178
187
|
metaTitle: string | null;
|
|
179
188
|
metaDescription: string | null;
|
|
180
189
|
}
|
|
190
|
+
/** A navigation menu — GET /store/menus/{handle}. */
|
|
191
|
+
export interface StoreMenu {
|
|
192
|
+
handle: string;
|
|
193
|
+
title: string;
|
|
194
|
+
/** Only items a shopper can follow, in the merchant's order; at most three levels. */
|
|
195
|
+
items: StoreMenuItem[];
|
|
196
|
+
}
|
|
197
|
+
export interface StoreMenuItem {
|
|
198
|
+
/** In the client's language when the store translated it, else the primary. */
|
|
199
|
+
label: string;
|
|
200
|
+
/**
|
|
201
|
+
* A path of the store without a language prefix (`/products/robe`) — pass
|
|
202
|
+
* it through `localizeHref` — or the absolute URL the merchant entered.
|
|
203
|
+
*/
|
|
204
|
+
href: string;
|
|
205
|
+
/** The page type it opens (`home`, `catalog`, `category`, `product`, `page`, `landing`), or `url`. */
|
|
206
|
+
type: string;
|
|
207
|
+
children: StoreMenuItem[];
|
|
208
|
+
}
|
|
181
209
|
export interface ShippingRates {
|
|
182
210
|
[wilayaId: string]: WilayaRates;
|
|
183
211
|
}
|
|
@@ -395,6 +423,19 @@ export type OtpVerifyResult = {
|
|
|
395
423
|
/** Signed, 15-min TTL, bound to the phone. Travels with the order. */
|
|
396
424
|
otpToken: string;
|
|
397
425
|
};
|
|
426
|
+
/** A photo or the voice note of a review. */
|
|
427
|
+
export interface ReviewMedia {
|
|
428
|
+
id: string;
|
|
429
|
+
kind: "image" | "audio";
|
|
430
|
+
/** A photo at 1600 px at most, or the voice note as recorded. */
|
|
431
|
+
url: string;
|
|
432
|
+
/** A photo's 480 px version for lists; null for a voice note. */
|
|
433
|
+
thumbUrl: string | null;
|
|
434
|
+
width: number | null;
|
|
435
|
+
height: number | null;
|
|
436
|
+
/** A voice note's length, when known. */
|
|
437
|
+
durationMs: number | null;
|
|
438
|
+
}
|
|
398
439
|
export interface Review {
|
|
399
440
|
id: string;
|
|
400
441
|
customerName: string;
|
|
@@ -402,6 +443,50 @@ export interface Review {
|
|
|
402
443
|
title: string | null;
|
|
403
444
|
body: string;
|
|
404
445
|
createdAt: string;
|
|
446
|
+
/** Rests on an order that reached its customer: show it as a verified purchase. */
|
|
447
|
+
verified: boolean;
|
|
448
|
+
/** Photos first, then the voice note. Empty from servers before 1.6. */
|
|
449
|
+
media: ReviewMedia[];
|
|
450
|
+
}
|
|
451
|
+
/** One product of the shopper's order, as `products.reviews.eligibility()` answers it. */
|
|
452
|
+
export interface ReviewableProduct {
|
|
453
|
+
productId: string;
|
|
454
|
+
name: string;
|
|
455
|
+
imageSrc: string | null;
|
|
456
|
+
/** Already reviewed from this order: show it, but it cannot be reviewed again. */
|
|
457
|
+
reviewed: boolean;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* What a shopper's order lets them review, once its number and phone proved
|
|
461
|
+
* the purchase — and, when the store takes them, what they may attach.
|
|
462
|
+
*/
|
|
463
|
+
export interface ReviewEligibility {
|
|
464
|
+
orderNumber: string;
|
|
465
|
+
customerName: string;
|
|
466
|
+
products: ReviewableProduct[];
|
|
467
|
+
/** null: text only. */
|
|
468
|
+
media: {
|
|
469
|
+
/** 0: no photos. */
|
|
470
|
+
maxPhotos: number;
|
|
471
|
+
/** null: no voice note. */
|
|
472
|
+
voiceMaxSeconds: number | null;
|
|
473
|
+
/** Send with each upload and with the review. Lasts until `expiresAt`. */
|
|
474
|
+
grant: string;
|
|
475
|
+
/** Unix seconds. */
|
|
476
|
+
expiresAt: number;
|
|
477
|
+
} | null;
|
|
478
|
+
}
|
|
479
|
+
/** A photo or voice note the server received for a review not yet submitted. */
|
|
480
|
+
export interface ReviewUpload {
|
|
481
|
+
uploadId: string;
|
|
482
|
+
kind: "image" | "audio";
|
|
483
|
+
bytes: number;
|
|
484
|
+
}
|
|
485
|
+
/** What a submitted review answers: published now, or after the store checks it. */
|
|
486
|
+
export interface ReviewSubmitResult {
|
|
487
|
+
id: string;
|
|
488
|
+
/** Absent from servers before 1.6. */
|
|
489
|
+
visibility?: "published" | "after_review" | undefined;
|
|
405
490
|
}
|
|
406
491
|
/**
|
|
407
492
|
* Filters `products.list()` accepts — named so a theme can type its own
|
|
@@ -427,15 +512,42 @@ export interface ReviewListPagination {
|
|
|
427
512
|
offset?: number;
|
|
428
513
|
}
|
|
429
514
|
/**
|
|
430
|
-
* The body `products.reviews.submit()` accepts: the order number
|
|
431
|
-
*
|
|
432
|
-
*
|
|
515
|
+
* The body `products.reviews.submit()` accepts: the order number and the phone
|
|
516
|
+
* it was placed with prove the purchase, and the server owns every other rule
|
|
517
|
+
* (a delivered order, one review per product of it, moderation).
|
|
433
518
|
*/
|
|
434
519
|
export interface ReviewInput {
|
|
435
520
|
orderNumber: string;
|
|
521
|
+
/**
|
|
522
|
+
* The phone the order was placed with. Without it (the 1.5 shape) a review
|
|
523
|
+
* is text only and still needs a delivered order.
|
|
524
|
+
* @deprecated Omitting it is deprecated; a later major requires it.
|
|
525
|
+
*/
|
|
526
|
+
phone?: string;
|
|
436
527
|
productId: string;
|
|
437
528
|
/** 1–5, the server validates the range. */
|
|
438
529
|
rating: number;
|
|
439
530
|
title?: string;
|
|
531
|
+
/** At least 10 characters, unless a voice note is attached. */
|
|
440
532
|
body: string;
|
|
533
|
+
/** The grant from `eligibility()`, when files are attached. */
|
|
534
|
+
grant?: string;
|
|
535
|
+
/** `uploadId`s from `upload()`. */
|
|
536
|
+
uploads?: string[];
|
|
537
|
+
}
|
|
538
|
+
/** What `products.reviews.eligibility()` sends. */
|
|
539
|
+
export interface ReviewEligibilityInput {
|
|
540
|
+
orderNumber: string;
|
|
541
|
+
phone: string;
|
|
542
|
+
/** Required when the store has Turnstile on. */
|
|
543
|
+
turnstileToken?: string;
|
|
544
|
+
}
|
|
545
|
+
/** What `products.reviews.upload()` sends: one file, as it is. */
|
|
546
|
+
export interface ReviewUploadInput {
|
|
547
|
+
grant: string;
|
|
548
|
+
body: Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
|
|
549
|
+
/** Its size, when known: lets the server refuse a file too large before it arrives. */
|
|
550
|
+
contentLength?: number;
|
|
551
|
+
/** A voice note's length as the recorder reported it. */
|
|
552
|
+
durationMs?: number;
|
|
441
553
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wocart/storefront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wocart",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/astro.d.ts",
|
|
42
42
|
"default": "./dist/astro.js"
|
|
43
43
|
},
|
|
44
|
+
"./review-media": {
|
|
45
|
+
"types": "./dist/review-media.d.ts",
|
|
46
|
+
"default": "./dist/review-media.js"
|
|
47
|
+
},
|
|
44
48
|
"./package.json": "./package.json"
|
|
45
49
|
},
|
|
46
50
|
"files": [
|