@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/dist/schemas.js CHANGED
@@ -15,24 +15,18 @@ export const ProductImageSchema = MediaImageSchema.extend({
15
15
  altText: z.string().nullable(),
16
16
  position: z.number().int().min(0),
17
17
  });
18
+ /** One active variant of a product, in the merchant's order; its selection in the client's language. */
18
19
  export const ProductVariantSchema = z.object({
19
20
  id: z.string(),
20
- productId: z.string(),
21
21
  variations: z.record(z.string(), z.string()),
22
- currency: z.string(),
23
22
  price: z.number().int().min(0),
24
23
  compareAtPrice: z.number().int().min(0).nullable(),
25
24
  sku: z.string().nullable(),
26
- barcode: z.string().nullable(),
27
25
  inventory: z.number().int().min(0),
28
26
  lowStockThreshold: z.number().int().min(0),
29
- weightKg: z.number().min(0).nullable(),
27
+ /** The product image this variant shows. */
30
28
  imageId: z.string().nullable(),
31
29
  isDefault: z.boolean(),
32
- active: z.boolean(),
33
- position: z.number().int().min(0),
34
- createdAt: z.string().datetime(),
35
- updatedAt: z.string().datetime(),
36
30
  });
37
31
  export const OfferSchema = z.object({
38
32
  id: z.string(),
@@ -45,9 +39,8 @@ export const OfferSchema = z.object({
45
39
  rewardProductName: z.string(),
46
40
  rewardVariantId: z.string().nullable(),
47
41
  rewardVariantLabel: z.string().nullable(),
48
- // What one gift unit is worth at list price. Optional: a server that predates
49
- // it omits the field, and the product's own price stands in for the gift.
50
- rewardUnitPrice: z.number().nullish(),
42
+ /** What one gift unit is worth at list price: the reward variant's price, or its product's. */
43
+ rewardUnitPrice: z.number().nullable(),
51
44
  });
52
45
  /**
53
46
  * Quantity pricing — price tiers by quantity ("2 for 3,500 DA"). The highest
@@ -83,6 +76,18 @@ const reviewStatsSchema = z.object({
83
76
  avgRating: z.number(),
84
77
  reviewCount: z.number().int(),
85
78
  });
79
+ const variantOptionSchema = z.object({
80
+ name: z.string(),
81
+ values: z.array(z.object({
82
+ value: z.string(),
83
+ // The key may be ABSENT, not just null: rows written before the colour
84
+ // picker existed never carry it, and a strict `.nullable()` would reject
85
+ // the whole product, since a failed read degrades to null. Normalising to
86
+ // null keeps the public shape `string | null` for every consumer.
87
+ hexColor: z.string().nullable().default(null),
88
+ })),
89
+ });
90
+ /** A product as a storefront reads it, in the client's language — the fields a listing and a product page share. */
86
91
  const productBaseFields = {
87
92
  id: z.string(),
88
93
  name: z.string(),
@@ -93,71 +98,35 @@ const productBaseFields = {
93
98
  currency: z.string(),
94
99
  price: z.number(),
95
100
  compareAtPrice: z.number().nullable(),
96
- /**
97
- * @deprecated Always null: what the merchant paid is never served to a
98
- * storefront. Older servers sent it; a later major drops it. Do not read it.
99
- */
100
- costPrice: z.number().nullable(),
101
101
  type: z.enum(["PHYSICAL", "DIGITAL"]),
102
102
  hasVariants: z.boolean(),
103
+ /** The option axes a shopper picks from; null for a product without variants. */
104
+ variantOptions: z.array(variantOptionSchema).nullable(),
103
105
  sku: z.string().nullable(),
104
106
  inventory: z.number().int(),
105
107
  trackInventory: z.boolean(),
106
108
  lowStockThreshold: z.number().int(),
107
109
  categoryId: z.string().nullable(),
108
- visibility: z.boolean(),
109
- status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
110
- showInStore: z.boolean(),
111
- /** @deprecated Whether the product is in the `featured` collection; list it with `collection: "featured"`. Removed in 2.0. */
112
- storeFeatured: z.boolean(),
113
- /**
114
- * @deprecated Always null: the server never serves a removed product. The
115
- * server still sends it for older releases; it will stop, and a later major
116
- * drops it. Do not read it.
117
- */
118
- deletedAt: z.null().optional(),
119
- publishedAt: z.string().datetime().nullable(),
120
- createdAt: z.string().datetime(),
121
- updatedAt: z.string().datetime(),
122
- shippingProfileId: z.string().nullable().optional(),
123
- /** The title search engines and shared links show, in the client's language; null or absent: use `productMeta(product)`, which falls back to the name. */
124
- metaTitle: z.string().nullish(),
125
- /** The description search engines and shared links show; null or absent: `productMeta(product)` shortens the product's own. */
126
- metaDescription: z.string().nullish(),
110
+ tags: z.array(z.string()),
111
+ /** The title search engines and shared links show; null: `productMeta(product)` falls back to the name. */
112
+ metaTitle: z.string().nullable(),
113
+ /** The description search engines and shared links show; null: `productMeta(product)` shortens the product's own. */
114
+ metaDescription: z.string().nullable(),
115
+ reviewStats: reviewStatsSchema.nullable(),
127
116
  };
128
- const variantOptionSchema = z.object({
129
- name: z.string(),
130
- values: z.array(z.object({
131
- value: z.string(),
132
- // The key may be ABSENT, not just null: rows written before the colour
133
- // picker existed never carry it, and a strict `.nullable()` would reject
134
- // the whole product, since a failed read degrades to null. Normalising to
135
- // null keeps the public shape `string | null` for every consumer.
136
- hexColor: z.string().nullable().default(null),
137
- })),
138
- });
139
- const rawProductFields = {
117
+ /** A product in a listing: its first photo stands for it. */
118
+ export const ProductListSchema = z.object({
140
119
  ...productBaseFields,
141
- variantOptions: z.string().nullable(),
142
- /** The tags as stored JSON text; read them with `productTags(product)`, which a product page's list also goes through. */
143
- tags: z.string().nullable(),
144
120
  coverImage: ProductImageSchema.nullable(),
145
- reviewStats: reviewStatsSchema.nullable(),
146
- };
147
- export const ProductListSchema = z.object(rawProductFields);
121
+ });
122
+ /** A product's page: every photo, variant and offer, and its category. */
148
123
  export const ProductSchema = z.object({
149
124
  ...productBaseFields,
150
- variantOptions: z.array(variantOptionSchema).nullable(),
151
- tags: z.array(z.string()),
152
125
  category: productCategorySchema.nullable(),
153
126
  variants: z.array(ProductVariantSchema),
154
127
  images: z.array(ProductImageSchema),
155
- reviewStats: reviewStatsSchema.nullable(),
156
- /**
157
- * The product's live quantity pricing; null or absent when it has none
158
- * (absent from a server that predates it).
159
- */
160
- quantityPricing: QuantityPricingSchema.nullish(),
128
+ /** The product's live quantity pricing; null when it has none. */
129
+ quantityPricing: QuantityPricingSchema.nullable(),
161
130
  offers: z.array(OfferSchema),
162
131
  });
163
132
  export const StorePagePublicSchema = z.object({
@@ -186,10 +155,10 @@ export const StoreCollectionSchema = z.object({
186
155
  handle: z.string(),
187
156
  title: z.string(),
188
157
  description: z.string().nullable(),
189
- /** How many products a shopper sees in it; absent from a server that predates it. */
190
- productCount: z.number().int().min(0).nullish(),
191
- /** Its first listed product's first photo; absent from a server that predates it. */
192
- image: ProductImageSchema.nullish(),
158
+ /** How many products a shopper sees in it. */
159
+ productCount: z.number().int().min(0),
160
+ /** Its first listed product's first photo; null while it lists none with one. */
161
+ image: ProductImageSchema.nullable(),
193
162
  });
194
163
  export const StoreMenuSchema = z.object({
195
164
  handle: z.string(),
@@ -218,11 +187,10 @@ export const StorePageLinkSchema = z.object({
218
187
  title: z.string(),
219
188
  position: z.number().int(),
220
189
  });
221
- export const StoreLegalContactSchema = z.object({
222
- contactEmail: z.string().nullable(),
223
- contactPhone: z.string().nullable(),
224
- deliveryMinDays: z.number().int(),
225
- deliveryMaxDays: z.number().int(),
190
+ export const StoreContactSchema = z.object({
191
+ phone: z.string().nullable(),
192
+ email: z.string().nullable(),
193
+ address: z.string().nullable(),
226
194
  });
227
195
  export const StoreWhatsAppWidgetSchema = z.object({
228
196
  href: z.string(),
@@ -234,8 +202,8 @@ export const StoreWhatsAppWidgetSchema = z.object({
234
202
  ctaLabel: z.string().nullable(),
235
203
  prefillGeneral: z.string().nullable(),
236
204
  prefillProduct: z.string().nullable(),
237
- thankYouButtonLabel: z.string().nullable().default(null),
238
- prefillThankYou: z.string().nullable().default(null),
205
+ thankYouButtonLabel: z.string().nullable(),
206
+ prefillThankYou: z.string().nullable(),
239
207
  accent: z.enum(["whatsapp", "primary"]),
240
208
  position: z.enum(["right", "left"]),
241
209
  attention: z.boolean(),
@@ -273,12 +241,6 @@ export const StoreConfigSchema = z.object({
273
241
  name: z.string(),
274
242
  domain: z.string().nullable(),
275
243
  logo: MediaImageSchema.nullable(),
276
- themeId: z.string(),
277
- primaryColor: z.string(),
278
- accentColor: z.string(),
279
- bgColor: z.string(),
280
- fontFamily: z.string(),
281
- fontUrl: z.string().nullable(),
282
244
  lang: z.string().min(1),
283
245
  locale: z.string().min(1),
284
246
  /**
@@ -286,44 +248,28 @@ export const StoreConfigSchema = z.object({
286
248
  * server owns which languages exist, so a new one never fails this parse.
287
249
  */
288
250
  locales: z.array(StorefrontLanguageSchema),
289
- /** The requesting theme's text edits, one map per published language. */
290
- contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
291
- /**
292
- * The store's values for the requesting theme's settings, as saved. Absent
293
- * from a server older than theme settings.
294
- */
295
- themeSettings: z.record(z.string(), z.unknown()).optional(),
296
251
  currency: z.string(),
297
252
  currencySymbol: z.string(),
298
253
  metaTitle: z.string().nullable(),
299
254
  metaDescription: z.string().nullable(),
300
255
  ogImage: MediaImageSchema.nullable(),
301
256
  media: MediaDeliverySchema,
302
- announcementBar: z.string().nullable(),
303
257
  reviewsEnabled: z.boolean(),
304
258
  cartEnabled: z.boolean(),
305
259
  freeShippingThreshold: z.number().int().nullable(),
306
- cartShippingMode: z.enum(["highest", "default_profile"]).default("highest"),
260
+ /** The requesting theme's text edits, one map per published language. */
261
+ contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
262
+ /** The store's values for the requesting theme's settings, as saved. */
263
+ themeSettings: z.record(z.string(), z.unknown()),
264
+ checkoutForm: CheckoutFormPolicySchema,
265
+ whatsapp: StoreWhatsAppWidgetSchema.nullable(),
266
+ pixelId: z.string().nullable(),
267
+ conversionEvent: z.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"]),
268
+ pages: z.array(StorePageLinkSchema),
269
+ contact: StoreContactSchema,
307
270
  otpEnabled: z.boolean(),
308
271
  turnstileEnabled: z.boolean(),
309
272
  turnstileSiteKey: z.string().nullable(),
310
- status: z.enum(["active", "inactive"]),
311
- createdAt: z.string().optional().default(() => new Date().toISOString()),
312
- updatedAt: z.string().optional().default(() => new Date().toISOString()),
313
- checkoutForm: CheckoutFormPolicySchema.default({
314
- address: "optional",
315
- notes: "optional",
316
- email: "optional",
317
- deliveryOptions: { home: true, stopDesk: true },
318
- customFields: [],
319
- }),
320
- pixelId: z.string().nullable().default(null),
321
- conversionEvent: z
322
- .enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"])
323
- .default("Purchase"),
324
- pages: z.array(StorePageLinkSchema).default([]),
325
- legalContact: StoreLegalContactSchema.nullable().default(null),
326
- whatsapp: StoreWhatsAppWidgetSchema.nullable().default(null),
327
273
  });
328
274
  export const LandingPageImageSchema = MediaImageSchema.extend({
329
275
  id: z.string(),
@@ -341,7 +287,7 @@ export const LandingPageSchema = z.object({
341
287
  publishedAt: z.string().nullable(),
342
288
  images: z.array(LandingPageImageSchema),
343
289
  product: ProductSchema.nullable(),
344
- document: PageDocumentPayloadSchema.nullable().catch(null).default(null),
290
+ document: PageDocumentPayloadSchema.nullable().catch(null),
345
291
  tracking: z.object({
346
292
  pixelId: z.string().nullable(),
347
293
  conversionEvent: z.enum([
@@ -370,24 +316,24 @@ export const CartValidationLineSchema = z.object({
370
316
  quantity: z.number().int(),
371
317
  unitPrice: z.number(),
372
318
  lineTotal: z.number(),
373
- /** The offer applied to the line; absent from a server that predates offers on lines. */
374
- offerId: z.string().nullish(),
375
- /** That offer's name, in the shopper's language; absent from an older server. */
376
- offerName: z.string().nullish(),
377
- /** The line's share of the coupon's discount, already out of `lineTotal`; absent from a server that predates coupons. */
378
- discount: z.number().nullish(),
319
+ /** The offer applied to the line. */
320
+ offerId: z.string().nullable(),
321
+ /** That offer's name, in the shopper's language. */
322
+ offerName: z.string().nullable(),
323
+ /** The line's share of the coupon's discount, already out of `lineTotal`. */
324
+ discount: z.number(),
379
325
  maxQuantity: z.number().nullable(),
380
326
  blocker: z.enum(["missing", "unlisted", "out_of_stock"]).nullable(),
381
327
  });
382
328
  export const CartRewardSchema = z.object({
383
329
  offerId: z.string(),
384
- /** The offer's name, in the shopper's language; absent from an older server. */
385
- offerName: z.string().nullish(),
330
+ /** The offer's name, in the shopper's language. */
331
+ offerName: z.string().nullable(),
386
332
  productId: z.string().nullable(),
387
333
  productName: z.string().nullable(),
388
- /** The variant given and its label; absent from an older server. */
389
- variantId: z.string().nullish(),
390
- variantLabel: z.string().nullish(),
334
+ /** The variant given and its label; null for a product without variants. */
335
+ variantId: z.string().nullable(),
336
+ variantLabel: z.string().nullable(),
391
337
  quantity: z.number().int(),
392
338
  });
393
339
  /**
@@ -409,6 +355,8 @@ export const CartValidationSchema = z.object({
409
355
  threshold: z.number().nullable(),
410
356
  qualified: z.boolean(),
411
357
  remaining: z.number(),
358
+ /** Why delivery is free: an offer, the coupon or the store's amount; null when it is not. */
359
+ by: z.enum(["offer", "coupon", "threshold"]).nullable(),
412
360
  }),
413
361
  delivery: z
414
362
  .discriminatedUnion("available", [
@@ -416,8 +364,8 @@ export const CartValidationSchema = z.object({
416
364
  z.object({ available: z.literal(false), fee: z.null(), total: z.null() }),
417
365
  ])
418
366
  .optional(),
419
- /** The code's outcome: null without one; absent from a server that predates coupons. */
420
- coupon: CouponOutcomeSchema.nullish(),
367
+ /** The code's outcome: null without one. */
368
+ coupon: CouponOutcomeSchema.nullable(),
421
369
  });
422
370
  export const CreatedOrderSchema = z.object({
423
371
  orderId: z.string(),
@@ -425,8 +373,8 @@ export const CreatedOrderSchema = z.object({
425
373
  price: z.number(),
426
374
  deliveryFee: z.number(),
427
375
  total: z.number(),
428
- /** What the coupon took off; absent from a server that predates coupons. */
429
- couponDiscount: z.number().nullish(),
376
+ /** What the coupon took off. */
377
+ couponDiscount: z.number(),
430
378
  });
431
379
  export const OrderTrackingSchema = z.object({
432
380
  pixelId: z.string().nullable(),
@@ -445,10 +393,10 @@ export const ShopperOrderStatusSchema = z.object({
445
393
  total: z.number(),
446
394
  tracking: z.object({ number: z.string(), carrier: z.string().nullable() }).nullable(),
447
395
  });
448
- /** A page of products with the whole listing's size; `total` is absent from a server that predates it. */
396
+ /** A page of products with the whole listing's size. */
449
397
  export const ProductPageEnvelopeSchema = z.object({
450
398
  data: z.array(ProductListSchema),
451
- total: z.number().int().min(0).optional(),
399
+ total: z.number().int().min(0),
452
400
  });
453
401
  /**
454
402
  * The `{ id }` success payload of the write endpoints that only mint a row:
@@ -483,25 +431,17 @@ export const ReviewMediaSchema = z.object({
483
431
  height: z.number().int().nullable(),
484
432
  durationMs: z.number().int().nullable(),
485
433
  });
486
- export const ReviewSchema = z.preprocess((raw) => {
487
- if (typeof raw === "object" && raw !== null) {
488
- const obj = raw;
489
- return {
490
- ...obj,
491
- customerName: obj.customerName ?? obj.authorName ?? "Customer",
492
- };
493
- }
494
- return raw;
495
- }, z.object({
434
+ /** An approved review, newest first: who wrote it, and whether a delivered order proves the purchase. */
435
+ export const ReviewSchema = z.object({
496
436
  id: z.string(),
497
437
  customerName: z.string(),
498
438
  rating: z.number(),
499
- title: z.string().nullable().optional().default(null),
439
+ title: z.string().nullable(),
500
440
  body: z.string(),
501
441
  createdAt: z.string(),
502
- verified: z.boolean().optional().default(false),
503
- media: z.array(ReviewMediaSchema).optional().default([]),
504
- }));
442
+ verified: z.boolean(),
443
+ media: z.array(ReviewMediaSchema),
444
+ });
505
445
  export const ReviewEligibilitySchema = z.object({
506
446
  orderNumber: z.string(),
507
447
  customerName: z.string(),
@@ -527,7 +467,7 @@ export const ReviewUploadSchema = z.object({
527
467
  });
528
468
  export const ReviewSubmitResultSchema = z.object({
529
469
  id: z.string(),
530
- visibility: z.enum(["published", "after_review"]).optional(),
470
+ visibility: z.enum(["published", "after_review"]),
531
471
  });
532
472
  /**
533
473
  * GET /store/reviews answers `{ success, data: rows, count, total }` — the
package/dist/seo.js CHANGED
@@ -44,7 +44,7 @@ const AVAILABILITY = { inStock: "https://schema.org/InStock", outOfStock: "https
44
44
  export function productStructuredData(product, options) {
45
45
  const meta = productMeta(product);
46
46
  const view = getProductView(product);
47
- const variants = product.hasVariants ? product.variants.filter((variant) => variant.active) : [];
47
+ const variants = product.hasVariants ? product.variants : [];
48
48
  const prices = variants.map((variant) => variant.price);
49
49
  const sellable = product.hasVariants
50
50
  ? variants.some((variant) => !product.trackInventory || variant.inventory > 0)
@@ -55,7 +55,7 @@ export function productStructuredData(product, options) {
55
55
  ? { "@type": "AggregateOffer", url: options.url, priceCurrency: product.currency, lowPrice: Math.min(...prices), highPrice: Math.max(...prices), offerCount: prices.length, availability }
56
56
  : { "@type": "Offer", url: options.url, priceCurrency: product.currency, price: view.basePrice, availability, itemCondition: "https://schema.org/NewCondition" };
57
57
  const sku = product.sku ?? (product.hasVariants ? initialVariant(product)?.sku : null) ?? null;
58
- const images = [...product.images].sort((a, b) => a.position - b.position).map((image) => image.url);
58
+ const images = product.images.map((image) => image.url);
59
59
  const rating = product.reviewStats && product.reviewStats.reviewCount > 0 ? product.reviewStats : null;
60
60
  return {
61
61
  "@context": "https://schema.org",
@@ -14,8 +14,9 @@
14
14
  * language the draft asks for when the store serves it.
15
15
  *
16
16
  * The theme keeps what is its own: the client it creates (one per request),
17
- * the config it falls back to when the store cannot be read, and the context
18
- * it builds from what these return.
17
+ * the config it falls back to when the store cannot be read
18
+ * (`unavailableStoreConfig`), and the context it builds from what these
19
+ * return.
19
20
  */
20
21
  import type { StoreClient } from "./client/index.js";
21
22
  import { type LocaleRouting } from "./locale-routing.js";
@@ -24,7 +25,7 @@ import type { StoreConfig } from "./types.js";
24
25
  export interface StoreRequestOptions {
25
26
  /** The request's client in `locale`, or in the store's primary language when absent. */
26
27
  client: (locale?: string) => StoreClient;
27
- /** What renders when the store's config cannot be read: a shell with the checkout switched off. */
28
+ /** What renders when the store's config cannot be read: `unavailableStoreConfig(…)`. */
28
29
  fallback: StoreConfig;
29
30
  /** Whether a path serves part of a page rather than a page. Default: `/api/` and `/_server-islands/`. */
30
31
  embedded?: (pathname: string) => boolean;
@@ -60,7 +61,7 @@ export type StoreRequestResolution = ({
60
61
  * ```ts
61
62
  * const request = await resolveStoreRequest(
62
63
  * { url: context.url, referer: context.request.headers.get("Referer") },
63
- * { client: createStore, fallback: FALLBACK_CONFIG },
64
+ * { client: createStore, fallback: unavailableStoreConfig({ serverUrl: env.COD_SERVER_URL }) },
64
65
  * );
65
66
  * if (request.kind === "redirect") return context.redirect(request.location, request.status);
66
67
  * context.locals.store = buildContext(request);
@@ -38,7 +38,7 @@ function askingPage(url, referer) {
38
38
  * ```ts
39
39
  * const request = await resolveStoreRequest(
40
40
  * { url: context.url, referer: context.request.headers.get("Referer") },
41
- * { client: createStore, fallback: FALLBACK_CONFIG },
41
+ * { client: createStore, fallback: unavailableStoreConfig({ serverUrl: env.COD_SERVER_URL }) },
42
42
  * );
43
43
  * if (request.kind === "redirect") return context.redirect(request.location, request.status);
44
44
  * context.locals.store = buildContext(request);