@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/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 frozen contract. Adding a route is a deliberate platform change: a path
3
- * that renders a surface but is missing here is a surface the merchant cannot
4
- * switch on with any confidence.
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 const STOREFRONT_ROUTES = [
7
- { path: "/products/:handle", surface: "product" },
8
- { path: "/category/:slug", surface: "catalog" },
9
- { path: "/pages/:slug", surface: "pages" },
10
- { path: "/lp/:slug", surface: "landing" },
11
- { path: "/checkout", surface: "checkout" },
12
- { path: "/thank-you", surface: "thankYou" },
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
- * The merchant answers "which surfaces" (`surfaces[surface]`); this function
18
- * answers the platform half — which surface a pathname *is* — and it reads the
19
- * contract above:
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
- // Normalise: strip query/hash, strip trailing slashes (except root "/")
33
- const pathname = rawPathname.split("?")[0].split("#")[0].replace(/\/+$/, "") || "/";
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.ZodNullable<z.ZodString>;
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.ZodNullable<z.ZodString>;
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.ZodNullable<z.ZodString>;
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
- deletedAt: z.string().datetime().nullable(),
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 is the proof
431
- * of purchase, and the server owns every other rule (moderation, one-review
432
- * per order line).
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.5.0",
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": [