@wocart/storefront 1.11.0 → 1.12.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 +18 -0
- package/USAGE.md +30 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/product-view.js +2 -3
- package/dist/types.d.ts +1 -1
- package/dist/variant-picker.d.ts +52 -0
- package/dist/variant-picker.js +46 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.12.0 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Variant picker.** `initialVariant(product)`, `pickOption(product,
|
|
13
|
+
selection, option, value)`, `optionStates(product, selection)` and
|
|
14
|
+
`variantFor(variants, selection)` (`OptionSelection`, `OptionState`,
|
|
15
|
+
`PickerVariant`): the variant a page opens on, the variant a picked value
|
|
16
|
+
means — always one that exists — and each value's state.
|
|
17
|
+
- `CouponRefusalReason` names `offer_applied`: every line of the basket is
|
|
18
|
+
already priced by an offer, and a code covers only lines no offer holds.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `getProductView(product).defaultVariant` is the merchant's default only
|
|
23
|
+
when it can be sold, else the first variant that can — never a sold-out or
|
|
24
|
+
switched-off one while another is in stock.
|
|
25
|
+
|
|
8
26
|
## 1.11.0 — 2026-10-17
|
|
9
27
|
|
|
10
28
|
### Added
|
package/USAGE.md
CHANGED
|
@@ -223,6 +223,7 @@ server directly.
|
|
|
223
223
|
| `productImageSource` | Which photo represents a product (cover → gallery fallback) |
|
|
224
224
|
| `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
|
|
225
225
|
| `getOfferTierView` | Pure offer-tier price, compare-price, savings, and free-shipping projection; display-only, never order authority |
|
|
226
|
+
| `initialVariant`, `pickOption`, `optionStates`, `variantFor` | Option buttons: the variant a page opens on, the variant a picked value means, each value's state (available, sold out, not made) |
|
|
226
227
|
| `getQuantityTierViews` | A product's quantity-pricing tiers as tiles show them — price, per-unit price, saving against the unit price, free delivery; display-only, the total is the server's cart quote |
|
|
227
228
|
| `META_CURRENCY`, `buildMetaProductPayload`, `buildMetaViewContentPayload`, `buildMetaAddToCartPayload`, `buildMetaInitiateCheckoutPayload` | Browser-safe Meta event payloads with the platform currency and catalog shape |
|
|
228
229
|
| `createMetaPixelTracker` | Injected browser-pixel dispatch; the theme supplies the vendor global and keeps bootstrap/timing |
|
|
@@ -647,6 +648,33 @@ Show the tiers as choices; never compute the total yourself. The cart quote
|
|
|
647
648
|
delivery — and the order charges exactly that. A tier locked to one variant
|
|
648
649
|
(`triggerVariantId`) counts and prices only that variant.
|
|
649
650
|
|
|
651
|
+
`product.offers` lists only offers the shopper can earn today: an offer whose
|
|
652
|
+
reward is out of stock, or a tier that costs what the catalog does, is left
|
|
653
|
+
out until it can give something.
|
|
654
|
+
|
|
655
|
+
**Option buttons.** Never derive a variant from each option's first value: a
|
|
656
|
+
shirt made in white M and blue L has no "white L". `getProductView(product)
|
|
657
|
+
.defaultVariant` (the same as `initialVariant(product)`) is the variant a page
|
|
658
|
+
opens on — the merchant's default when it can be sold, else the first that
|
|
659
|
+
can. When the shopper picks a value, `pickOption` names the variant they mean
|
|
660
|
+
and the selection to show, keeping most of their other choices; the buttons
|
|
661
|
+
then always show a variant that exists, the one the order is placed for.
|
|
662
|
+
|
|
663
|
+
~~~ts
|
|
664
|
+
let selection = getProductView(product).defaultVariant?.variations ?? {};
|
|
665
|
+
function choose(option: string, value: string) {
|
|
666
|
+
const picked = pickOption(product, selection, option, value);
|
|
667
|
+
if (!picked.variant) return;
|
|
668
|
+
selection = picked.selection;
|
|
669
|
+
render(picked.variant, optionStates(product, selection));
|
|
670
|
+
// optionStates: { Colour: { White: "available", Black: "sold_out", Blue: "unavailable" }, … }
|
|
671
|
+
}
|
|
672
|
+
~~~
|
|
673
|
+
|
|
674
|
+
The picker reads only `variations`, `active`, `inventory`, `position` and
|
|
675
|
+
`isDefault` of each variant (`PickerVariant`), so a page may hand it a
|
|
676
|
+
lighter copy.
|
|
677
|
+
|
|
650
678
|
### Product lists
|
|
651
679
|
|
|
652
680
|
~~~ts
|
|
@@ -1081,7 +1109,8 @@ if (quoted.success && quoted.data.coupon) {
|
|
|
1081
1109
|
// means delivery.fee is 0.
|
|
1082
1110
|
// refused: coupon.reason — not_found, not_started (startsAt), expired,
|
|
1083
1111
|
// used_up, identify_first, not_eligible, already_used,
|
|
1084
|
-
// no_eligible_products,
|
|
1112
|
+
// no_eligible_products, offer_applied (an offer already prices
|
|
1113
|
+
// every line), below_minimum_subtotal / _quantity (shortBy),
|
|
1085
1114
|
// delivery_already_free. An unknown reason gets the theme's general sentence.
|
|
1086
1115
|
}
|
|
1087
1116
|
~~~
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export * from "./domain.js";
|
|
|
36
36
|
export * from "./product-view.js";
|
|
37
37
|
export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
|
|
38
38
|
export { COUPON_NOT_APPLICABLE, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf } from "./coupon.js";
|
|
39
|
+
export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
|
|
40
|
+
export type { OptionSelection, OptionState, PickerVariant } from "./variant-picker.js";
|
|
39
41
|
export type { CartQuoteOptions } from "./client/cart.js";
|
|
40
42
|
export type { OfferTierInput, OfferTierView, QuantityTierView } from "./offer-view.js";
|
|
41
43
|
export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
|
package/dist/index.js
CHANGED
|
@@ -27,5 +27,6 @@ export * from "./domain.js";
|
|
|
27
27
|
export * from "./product-view.js";
|
|
28
28
|
export { getOfferTierView, getQuantityTierViews } from "./offer-view.js";
|
|
29
29
|
export { COUPON_NOT_APPLICABLE, COUPON_URL_PARAM, couponFromUrl, couponRefusalOf } from "./coupon.js";
|
|
30
|
+
export { initialVariant, optionStates, pickOption, variantFor } from "./variant-picker.js";
|
|
30
31
|
export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
|
|
31
32
|
export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
|
package/dist/product-view.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initialVariant } from "./variant-picker.js";
|
|
1
2
|
function discountOf(basePrice, comparePrice) {
|
|
2
3
|
if (comparePrice == null || comparePrice <= basePrice) {
|
|
3
4
|
return { hasDiscount: false, discountPct: 0 };
|
|
@@ -17,9 +18,7 @@ function stockOf(product) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export function getProductView(product) {
|
|
20
|
-
const defaultVariant = product.hasVariants
|
|
21
|
-
? (product.variants.find((variant) => variant.isDefault) ?? product.variants[0] ?? null)
|
|
22
|
-
: null;
|
|
21
|
+
const defaultVariant = product.hasVariants ? initialVariant(product) : null;
|
|
23
22
|
const basePrice = defaultVariant?.price ?? product.price;
|
|
24
23
|
const comparePrice = defaultVariant?.compareAtPrice ?? product.compareAtPrice ?? null;
|
|
25
24
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -358,7 +358,7 @@ export interface CartValidation {
|
|
|
358
358
|
* Why a code does not apply. Open: a server may name a reason this SDK has not
|
|
359
359
|
* seen yet, and a theme then shows its general sentence.
|
|
360
360
|
*/
|
|
361
|
-
export type CouponRefusalReason = "not_found" | "not_started" | "expired" | "used_up" | "identify_first" | "not_eligible" | "already_used" | "no_eligible_products" | "below_minimum_subtotal" | "below_minimum_quantity" | "delivery_already_free" | (string & {});
|
|
361
|
+
export type CouponRefusalReason = "not_found" | "not_started" | "expired" | "used_up" | "identify_first" | "not_eligible" | "already_used" | "no_eligible_products" | "offer_applied" | "below_minimum_subtotal" | "below_minimum_quantity" | "delivery_already_free" | (string & {});
|
|
362
362
|
export interface CouponRefusal {
|
|
363
363
|
reason: CouponRefusalReason;
|
|
364
364
|
/** The dinars (`below_minimum_subtotal`) or items (`below_minimum_quantity`) still missing. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing a variant with option buttons — the rule every product page needs,
|
|
3
|
+
* so no theme guesses it.
|
|
4
|
+
*
|
|
5
|
+
* A page opens on a variant that can be sold, its buttons showing that
|
|
6
|
+
* variant's values. A value picked that does not combine with the other
|
|
7
|
+
* choices moves the selection to the variant that has it and keeps most of
|
|
8
|
+
* them, so what the buttons show is always a variant that exists — the one
|
|
9
|
+
* the order is placed for. Each value can be told apart: available, sold out,
|
|
10
|
+
* or not made with the other choices.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProductVariant } from "./types.js";
|
|
13
|
+
/** What the picker reads of a variant — a page may hand it a lighter copy than `ProductVariant`. */
|
|
14
|
+
export type PickerVariant = Pick<ProductVariant, "variations" | "active" | "inventory" | "position" | "isDefault">;
|
|
15
|
+
/** Option name → chosen value, as a product's `variantOptions` name them. */
|
|
16
|
+
export type OptionSelection = Readonly<Record<string, string>>;
|
|
17
|
+
export type OptionState = "available" | "sold_out" | "unavailable";
|
|
18
|
+
interface Catalog<V extends PickerVariant> {
|
|
19
|
+
readonly variants: readonly V[];
|
|
20
|
+
readonly trackInventory: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** The variant whose every option has the value `selection` gives it, or null. */
|
|
23
|
+
export declare function variantFor<V extends PickerVariant>(variants: readonly V[], selection: OptionSelection): V | null;
|
|
24
|
+
/**
|
|
25
|
+
* The variant a product page opens on: the merchant's default when it can be
|
|
26
|
+
* sold, else the first that can, else the default (or first) shown sold out.
|
|
27
|
+
*/
|
|
28
|
+
export declare function initialVariant<V extends PickerVariant>(product: Catalog<V>): V | null;
|
|
29
|
+
/**
|
|
30
|
+
* The shopper picks `value` for `option`. The variant they mean is the one
|
|
31
|
+
* with that value that keeps most of their other choices — one that can be
|
|
32
|
+
* sold before one that cannot, then the catalog's order. Null, with the
|
|
33
|
+
* selection unchanged, when no variant has the value.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pickOption<V extends PickerVariant>(product: Catalog<V>, selection: OptionSelection, option: string, value: string): {
|
|
36
|
+
variant: V | null;
|
|
37
|
+
selection: OptionSelection;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Each value of each option, given the other choices: `available` when a
|
|
41
|
+
* variant with it and the other choices can be sold, `sold_out` when that
|
|
42
|
+
* variant exists without stock, `unavailable` when it is not made.
|
|
43
|
+
*/
|
|
44
|
+
export declare function optionStates<V extends PickerVariant>(product: Catalog<V> & {
|
|
45
|
+
readonly variantOptions?: readonly {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly values: readonly {
|
|
48
|
+
readonly value: string;
|
|
49
|
+
}[];
|
|
50
|
+
}[] | null;
|
|
51
|
+
}, selection: OptionSelection): Record<string, Record<string, OptionState>>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const live = (variant) => variant.active;
|
|
2
|
+
const sellable = (product, variant) => live(variant) && (!product.trackInventory || variant.inventory > 0);
|
|
3
|
+
const byPosition = (a, b) => a.position - b.position;
|
|
4
|
+
/** The variant whose every option has the value `selection` gives it, or null. */
|
|
5
|
+
export function variantFor(variants, selection) {
|
|
6
|
+
return variants.find((variant) => live(variant) && Object.entries(variant.variations).every(([option, value]) => selection[option] === value)) ?? null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The variant a product page opens on: the merchant's default when it can be
|
|
10
|
+
* sold, else the first that can, else the default (or first) shown sold out.
|
|
11
|
+
*/
|
|
12
|
+
export function initialVariant(product) {
|
|
13
|
+
const ordered = product.variants.filter(live).sort(byPosition);
|
|
14
|
+
const preferred = ordered.find((variant) => variant.isDefault) ?? ordered[0] ?? null;
|
|
15
|
+
if (preferred && sellable(product, preferred))
|
|
16
|
+
return preferred;
|
|
17
|
+
return ordered.find((variant) => sellable(product, variant)) ?? preferred;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The shopper picks `value` for `option`. The variant they mean is the one
|
|
21
|
+
* with that value that keeps most of their other choices — one that can be
|
|
22
|
+
* sold before one that cannot, then the catalog's order. Null, with the
|
|
23
|
+
* selection unchanged, when no variant has the value.
|
|
24
|
+
*/
|
|
25
|
+
export function pickOption(product, selection, option, value) {
|
|
26
|
+
const kept = (variant) => Object.entries(selection).filter(([name, chosen]) => variant.variations[name] === chosen).length;
|
|
27
|
+
const candidates = product.variants.filter((variant) => live(variant) && variant.variations[option] === value);
|
|
28
|
+
const best = candidates.sort((a, b) => kept(b) - kept(a) || Number(sellable(product, b)) - Number(sellable(product, a)) || byPosition(a, b))[0];
|
|
29
|
+
return best ? { variant: best, selection: { ...best.variations } } : { variant: null, selection };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Each value of each option, given the other choices: `available` when a
|
|
33
|
+
* variant with it and the other choices can be sold, `sold_out` when that
|
|
34
|
+
* variant exists without stock, `unavailable` when it is not made.
|
|
35
|
+
*/
|
|
36
|
+
export function optionStates(product, selection) {
|
|
37
|
+
const states = {};
|
|
38
|
+
for (const option of product.variantOptions ?? []) {
|
|
39
|
+
states[option.name] = {};
|
|
40
|
+
for (const { value } of option.values) {
|
|
41
|
+
const match = variantFor(product.variants, { ...selection, [option.name]: value });
|
|
42
|
+
states[option.name][value] = match === null ? "unavailable" : sellable(product, match) ? "available" : "sold_out";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return states;
|
|
46
|
+
}
|
package/package.json
CHANGED