@wocart/storefront 1.12.0 → 1.14.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 CHANGED
@@ -5,6 +5,31 @@ 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
+ ## 1.14.0 — 2026-10-18
9
+
10
+ ### Added
11
+
12
+ - `mediaImageUrl(url, params, mediaDomain)` (`MediaImageParams`): the
13
+ Cloudflare resizing URL of a media-domain image, served by the media domain
14
+ itself, so it works wherever the storefront is hosted; `null` for any other
15
+ image. Every image helper builds its URL with it, and so does
16
+ `@wocart/theme/image-service`.
17
+
18
+ ### Changed
19
+
20
+ - Every resizing URL starts with `onerror=redirect`: when a resize fails,
21
+ Cloudflare redirects to the original image instead of answering with an
22
+ error.
23
+
24
+ ## 1.13.0 — 2026-10-17
25
+
26
+ ### Added
27
+
28
+ - A cart quote names its offers: `CartValidationLine.offerName`, and each
29
+ `CartReward` says what will be given — `variantId`, `variantLabel` and the
30
+ giving offer's `offerName` — in the shopper's language. All absent from an
31
+ older server.
32
+
8
33
  ## 1.12.0 — 2026-10-17
9
34
 
10
35
  ### Added
package/USAGE.md CHANGED
@@ -214,6 +214,7 @@ server directly.
214
214
  | Export | Use it for |
215
215
  | --- | --- |
216
216
  | `formatMoney`, `optimizeImage`, `generateSrcSet` | Money text and image URLs |
217
+ | `mediaImageUrl` | The Cloudflare resizing URL of a media-domain image, served by the media domain itself (any storefront host); `null` for any other image — the builder behind every image helper and `@wocart/theme/image-service` |
217
218
  | `createImageHelpers(mediaDomain)`, `getCardImage`, `getGalleryImage`, `getThumbnailImage`, `getHeroImage`, `getCategoryImage`, `getLogoImage` | The bound image toolkit (`IMAGE_SIZES` holds the presets) |
218
219
  | `WILAYAS`, `ALGERIA_COMMUNES`, `communesForWilaya` | Reference data: 58 wilayas, 1,551 communes, one wilaya's rows |
219
220
  | `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
@@ -1000,10 +1001,17 @@ The server re-prices catalog lines, merges duplicate lines, checks stock and
1000
1001
  offers, and calculates free-delivery eligibility. CartValidation contains:
1001
1002
 
1002
1003
  - lines with productId, variantId, variantLabel, productName, quantity,
1003
- unitPrice, lineTotal, maxQuantity, and blocker;
1004
- - subtotal and rewards;
1004
+ unitPrice, lineTotal (offers and the coupon applied), offerId and
1005
+ offerName (the offer that priced the line), discount, maxQuantity, and
1006
+ blocker;
1007
+ - subtotal and rewards — each gift as the order will give it: productName,
1008
+ variantLabel, quantity and the offerName that gives it, free;
1005
1009
  - freeDelivery with fromOffer, threshold, qualified, and remaining.
1006
1010
 
1011
+ Show a line at `lineTotal + discount` (the coupon has its own row) and its
1012
+ catalog price struck through when an offer brought it down; list each reward
1013
+ as its own free row, so the basket reads like the order it becomes.
1014
+
1007
1015
  Line blockers include missing, unlisted, and out_of_stock. The creative
1008
1016
  experience is yours: inline warnings, a highlighted invalid line, a drawer,
1009
1017
  or a guided correction step. The server result is not optional.
package/dist/index.d.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  */
12
12
  export { formatMoney } from "./utils/money.js";
13
13
  export type { MoneyFormatOptions } from "./utils/money.js";
14
- export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
15
- export type { ImageHelpers, ImageResizeOptions } from "./utils/image.js";
14
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, } from "./utils/image.js";
15
+ export type { ImageHelpers, ImageResizeOptions, MediaImageParams } from "./utils/image.js";
16
16
  export { productImageSource } from "./utils/product-image.js";
17
17
  export type { ProductImageSourceInput } from "./utils/product-image.js";
18
18
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * Internal modules stay reachable only through their intentional entry points.
11
11
  */
12
12
  export { formatMoney } from "./utils/money.js";
13
- export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
13
+ export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, mediaImageUrl, } from "./utils/image.js";
14
14
  export { productImageSource } from "./utils/product-image.js";
15
15
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
16
16
  export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
package/dist/schemas.d.ts CHANGED
@@ -713,6 +713,7 @@ export declare const CartValidationLineSchema: z.ZodObject<{
713
713
  unitPrice: z.ZodNumber;
714
714
  lineTotal: z.ZodNumber;
715
715
  offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
716
+ offerName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
716
717
  discount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
717
718
  maxQuantity: z.ZodNullable<z.ZodNumber>;
718
719
  blocker: z.ZodNullable<z.ZodEnum<{
@@ -723,8 +724,11 @@ export declare const CartValidationLineSchema: z.ZodObject<{
723
724
  }, z.core.$strip>;
724
725
  export declare const CartRewardSchema: z.ZodObject<{
725
726
  offerId: z.ZodString;
727
+ offerName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
726
728
  productId: z.ZodNullable<z.ZodString>;
727
729
  productName: z.ZodNullable<z.ZodString>;
730
+ variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
731
+ variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
728
732
  quantity: z.ZodNumber;
729
733
  }, z.core.$strip>;
730
734
  /**
@@ -755,6 +759,7 @@ export declare const CartValidationSchema: z.ZodObject<{
755
759
  unitPrice: z.ZodNumber;
756
760
  lineTotal: z.ZodNumber;
757
761
  offerId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
762
+ offerName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
758
763
  discount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
759
764
  maxQuantity: z.ZodNullable<z.ZodNumber>;
760
765
  blocker: z.ZodNullable<z.ZodEnum<{
@@ -766,8 +771,11 @@ export declare const CartValidationSchema: z.ZodObject<{
766
771
  subtotal: z.ZodNumber;
767
772
  rewards: z.ZodArray<z.ZodObject<{
768
773
  offerId: z.ZodString;
774
+ offerName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
769
775
  productId: z.ZodNullable<z.ZodString>;
770
776
  productName: z.ZodNullable<z.ZodString>;
777
+ variantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
778
+ variantLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
771
779
  quantity: z.ZodNumber;
772
780
  }, z.core.$strip>>;
773
781
  freeDelivery: z.ZodObject<{
package/dist/schemas.js CHANGED
@@ -353,6 +353,8 @@ export const CartValidationLineSchema = z.object({
353
353
  lineTotal: z.number(),
354
354
  /** The offer applied to the line; absent from a server that predates offers on lines. */
355
355
  offerId: z.string().nullish(),
356
+ /** That offer's name, in the shopper's language; absent from an older server. */
357
+ offerName: z.string().nullish(),
356
358
  /** The line's share of the coupon's discount, already out of `lineTotal`; absent from a server that predates coupons. */
357
359
  discount: z.number().nullish(),
358
360
  maxQuantity: z.number().nullable(),
@@ -360,8 +362,13 @@ export const CartValidationLineSchema = z.object({
360
362
  });
361
363
  export const CartRewardSchema = z.object({
362
364
  offerId: z.string(),
365
+ /** The offer's name, in the shopper's language; absent from an older server. */
366
+ offerName: z.string().nullish(),
363
367
  productId: z.string().nullable(),
364
368
  productName: z.string().nullable(),
369
+ /** The variant given and its label; absent from an older server. */
370
+ variantId: z.string().nullish(),
371
+ variantLabel: z.string().nullish(),
365
372
  quantity: z.number().int(),
366
373
  });
367
374
  /**
package/dist/types.d.ts CHANGED
@@ -314,6 +314,8 @@ export interface CartValidationLine {
314
314
  lineTotal: number;
315
315
  /** The offer applied to the line; null or absent when none. */
316
316
  offerId?: string | null | undefined;
317
+ /** That offer's name, in the shopper's language; null or absent when none. */
318
+ offerName?: string | null | undefined;
317
319
  /** The line's share of the coupon's discount, already out of `lineTotal`; null or absent without one. */
318
320
  discount?: number | null | undefined;
319
321
  /** Units still available; null when the product is not stock-tracked. */
@@ -325,10 +327,16 @@ export interface CartValidationLine {
325
327
  blocker: "missing" | "unlisted" | "out_of_stock" | null;
326
328
  }
327
329
  /** A satisfied Buy-X-Get-Y reward, appended server-side at order time. */
330
+ /** A gift the basket earns, as the order will give it: free, beside the lines. */
328
331
  export interface CartReward {
329
332
  offerId: string;
333
+ /** The offer's name, in the shopper's language; absent from an older server. */
334
+ offerName?: string | null | undefined;
330
335
  productId: string | null;
331
336
  productName: string | null;
337
+ /** The variant given and its label; null for a product without variants, absent from an older server. */
338
+ variantId?: string | null | undefined;
339
+ variantLabel?: string | null | undefined;
332
340
  quantity: number;
333
341
  }
334
342
  /** The catalog's truth about a basket, right now. Read-only — nothing is written. */
@@ -29,10 +29,30 @@ export interface ImageResizeOptions {
29
29
  * { width: 400, height: 400 },
30
30
  * 'media.example.com'
31
31
  * );
32
- * // Returns: https://media.example.com/cdn-cgi/image/width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
32
+ * // Returns: https://media.example.com/cdn-cgi/image/onerror=redirect,width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
33
33
  * ```
34
34
  */
35
35
  export declare function optimizeImage(originalUrl: string, options: ImageResizeOptions, mediaDomain?: string): string;
36
+ /** What `mediaImageUrl` can ask Cloudflare for; an omitted value is left to Cloudflare's default. */
37
+ export interface MediaImageParams {
38
+ width?: number;
39
+ height?: number;
40
+ quality?: number;
41
+ format?: string;
42
+ fit?: "scale-down" | "contain" | "cover" | "crop" | "pad" | "squeeze";
43
+ }
44
+ /**
45
+ * The Cloudflare Image Resizing URL of an image on the media domain, served
46
+ * by the media domain itself (`https://<media>/cdn-cgi/image/<options>/<path>`)
47
+ * — so it works wherever the storefront is hosted, as long as the media
48
+ * domain's zone has Image Transformations on.
49
+ *
50
+ * `onerror=redirect` comes first: when a resize fails, Cloudflare redirects to
51
+ * the original image instead of answering with an error.
52
+ *
53
+ * @returns `null` when there is no media domain, or the URL is not on it.
54
+ */
55
+ export declare function mediaImageUrl(originalUrl: string, params: MediaImageParams, mediaDomain?: string): string | null;
36
56
  /**
37
57
  * Predefined image sizes for common use cases
38
58
  */
@@ -22,39 +22,46 @@
22
22
  * { width: 400, height: 400 },
23
23
  * 'media.example.com'
24
24
  * );
25
- * // Returns: https://media.example.com/cdn-cgi/image/width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
25
+ * // Returns: https://media.example.com/cdn-cgi/image/onerror=redirect,width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
26
26
  * ```
27
27
  */
28
28
  export function optimizeImage(originalUrl, options, mediaDomain) {
29
- // No-op when mediaDomain is empty or URL doesn't match
30
- if (!mediaDomain || !originalUrl) {
31
- return originalUrl;
32
- }
33
- const normalizedDomain = `https://${mediaDomain.replace(/^https?:\/\//, "")}`;
29
+ const { width, height, quality = 85, format = "webp", fit = "cover" } = options;
30
+ return mediaImageUrl(originalUrl, { width, height, format, quality, fit }, mediaDomain) ?? originalUrl;
31
+ }
32
+ /**
33
+ * The Cloudflare Image Resizing URL of an image on the media domain, served
34
+ * by the media domain itself (`https://<media>/cdn-cgi/image/<options>/<path>`)
35
+ * — so it works wherever the storefront is hosted, as long as the media
36
+ * domain's zone has Image Transformations on.
37
+ *
38
+ * `onerror=redirect` comes first: when a resize fails, Cloudflare redirects to
39
+ * the original image instead of answering with an error.
40
+ *
41
+ * @returns `null` when there is no media domain, or the URL is not on it.
42
+ */
43
+ export function mediaImageUrl(originalUrl, params, mediaDomain) {
44
+ if (!mediaDomain || !originalUrl)
45
+ return null;
34
46
  let mediaOrigin;
35
47
  let original;
36
48
  try {
37
- mediaOrigin = new URL(normalizedDomain).origin;
49
+ mediaOrigin = new URL(`https://${mediaDomain.replace(/^https?:\/\//, "")}`).origin;
38
50
  original = new URL(originalUrl);
39
51
  }
40
52
  catch {
41
- return originalUrl;
53
+ return null;
42
54
  }
43
- // Only optimize a URL from the configured media origin
44
- if (original.origin !== mediaOrigin) {
45
- return originalUrl;
55
+ if (original.origin !== mediaOrigin)
56
+ return null;
57
+ const options = ["onerror=redirect"];
58
+ for (const key of ["width", "height", "format", "quality", "fit"]) {
59
+ const value = params[key];
60
+ if (value !== undefined && value !== "")
61
+ options.push(`${key}=${value}`);
46
62
  }
47
- const { width, height, quality = 85, format = "webp", fit = "cover" } = options;
48
63
  const imagePath = `${original.pathname.replace(/^\/+/, "")}${original.search}${original.hash}`;
49
- // Build the Cloudflare Image Resizing URL
50
- const resizeParams = [
51
- `width=${width}`,
52
- `height=${height}`,
53
- `format=${format}`,
54
- `quality=${quality}`,
55
- `fit=${fit}`,
56
- ].join(",");
57
- return `${mediaOrigin}/cdn-cgi/image/${resizeParams}/${imagePath}`;
64
+ return `${mediaOrigin}/cdn-cgi/image/${options.join(",")}/${imagePath}`;
58
65
  }
59
66
  /**
60
67
  * Predefined image sizes for common use cases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.12.0",
3
+ "version": "1.14.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",