@wix/agent-skills 1.19.1 → 1.19.2
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/package.json +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +1 -1
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +7 -2
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +17 -4
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +3 -4
- package/skills/wix-base44-connector/SKILL.md +46 -24
- package/skills/wix-headless/SKILL.md +1 -0
- package/skills/wix-headless/references/CAPABILITIES.md +7 -2
- package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
- package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
- package/skills/wix-headless/references/managed/CREATE.md +5 -0
- package/skills/wix-manage/SKILL.md +4 -4
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
- package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
- package/skills/wix-manage/references/seo/manage-content-plan.md +1 -1
- package/skills/wix-manage/references/sites/upload-static-site.md +264 -34
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +68 -26
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { createContext, useContext, useState, useEffect, useCallback } from "react";
|
|
4
4
|
import {
|
|
5
5
|
getCurrentCart,
|
|
6
|
+
estimateCurrentCart,
|
|
6
7
|
addToCart as apiAdd,
|
|
7
8
|
removeFromCart as apiRemove,
|
|
8
9
|
updateCartItemQuantity as apiQty,
|
|
@@ -12,7 +13,11 @@ import {
|
|
|
12
13
|
const CartContext = createContext(null);
|
|
13
14
|
|
|
14
15
|
export function CartProvider({ children }) {
|
|
15
|
-
const [cart,
|
|
16
|
+
const [cart, setCartState] = useState(null);
|
|
17
|
+
// The calculated summary (after-discount subtotal, cart-level discount) — from the estimate, never
|
|
18
|
+
// summed in the client. Refreshed with every cart change; null while unknown or when the estimate
|
|
19
|
+
// fails, in which case surfaces fall back to cart.subtotal.
|
|
20
|
+
const [summary, setSummary] = useState(null);
|
|
16
21
|
const [isOpen, setIsOpen] = useState(false);
|
|
17
22
|
const [loading, setLoading] = useState(false);
|
|
18
23
|
// The cart helpers throw on refusal with a buyer-readable reason — an empty cart, or line items
|
|
@@ -20,7 +25,13 @@ export function CartProvider({ children }) {
|
|
|
20
25
|
// this the rejection is unhandled and the shopper sees the spinner stop with nothing said.
|
|
21
26
|
const [error, setError] = useState(null);
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
// Every cart write goes through here so the summary always describes the cart on screen.
|
|
29
|
+
const setCart = useCallback((next) => {
|
|
30
|
+
setCartState(next);
|
|
31
|
+
if (!next?.lineItems?.length) { setSummary(null); return; }
|
|
32
|
+
estimateCurrentCart().then((s) => setSummary(s)).catch(() => setSummary(null));
|
|
33
|
+
}, []);
|
|
34
|
+
const refreshCart = useCallback(async () => setCart(await getCurrentCart()), [setCart]);
|
|
24
35
|
useEffect(() => {
|
|
25
36
|
refreshCart();
|
|
26
37
|
const onVisible = () => document.visibilityState === "visible" && refreshCart();
|
|
@@ -59,7 +70,7 @@ export function CartProvider({ children }) {
|
|
|
59
70
|
run(async () => { window.location.href = await apiCheckout(); }, { reread: true });
|
|
60
71
|
|
|
61
72
|
return (
|
|
62
|
-
<CartContext.Provider value={{ cart, itemCount, isOpen, setIsOpen, loading, error, clearError: () => setError(null), addToCart, removeItem, updateQuantity, checkout, refreshCart }}>
|
|
73
|
+
<CartContext.Provider value={{ cart, summary, itemCount, isOpen, setIsOpen, loading, error, clearError: () => setError(null), addToCart, removeItem, updateQuantity, checkout, refreshCart }}>
|
|
63
74
|
{children}
|
|
64
75
|
</CartContext.Provider>
|
|
65
76
|
);
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
// Headless data layer for a product grid tile.
|
|
2
2
|
// Normalises raw Wix product fields into render-agnostic structures so you can build
|
|
3
3
|
// whatever card UI you want (image layout, badge style, price display, quick-add trigger)
|
|
4
|
-
// without touching the badge
|
|
4
|
+
// without touching the badge logic, price rules, or colour-dot extraction.
|
|
5
5
|
//
|
|
6
6
|
// Usage:
|
|
7
|
-
// const { isSoldOut, leftBadges, promoBadge, priceDisplay, compareAtDisplay,
|
|
8
|
-
// colors, optionLabel, isQuickAddable, image, hoverImage } = useProductCard(product);
|
|
9
|
-
// // then render however you want
|
|
7
|
+
// const { isSoldOut, leftBadges, ribbons, promoBadge, priceDisplay, compareAtDisplay,
|
|
8
|
+
// colors, optionLabel, isQuickAddable, directAddVariantId, image, hoverImage } = useProductCard(product);
|
|
9
|
+
// // then render however you want; quick-add: addToCart(product.id, directAddVariantId) from useCart().
|
|
10
10
|
|
|
11
11
|
import { useMemo } from "react";
|
|
12
12
|
import { productImage, productGallery } from "@/lib/storeImage";
|
|
13
|
-
|
|
14
|
-
function discountPercent(product) {
|
|
15
|
-
const now = Number(product?.actualPriceRange?.minValue?.amount);
|
|
16
|
-
const was = Number(product?.compareAtPriceRange?.minValue?.amount);
|
|
17
|
-
if (!now || !was || was <= now) return null;
|
|
18
|
-
return Math.round(((was - now) / was) * 100);
|
|
19
|
-
}
|
|
13
|
+
import { sellingPrice } from "@/rest/wix-store-catalog";
|
|
20
14
|
|
|
21
15
|
export function useProductCard(product) {
|
|
22
16
|
return useMemo(() => {
|
|
@@ -32,23 +26,31 @@ export function useProductCard(product) {
|
|
|
32
26
|
else if (isSoldOut) leftBadges.push({ type: "sold-out", label: "Sold out" });
|
|
33
27
|
if (isPartiallyOutOfStock) leftBadges.push({ type: "limited-stock", label: "Limited stock" });
|
|
34
28
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
? { type: "ribbon", label: ribbon }
|
|
43
|
-
: null;
|
|
29
|
+
// Merchant ribbons — the primary one plus every additional one. Render ALL of them, in one shared
|
|
30
|
+
// style (a "Sale" accent is fine, applied by label). Never compute a "-20%" badge from the price
|
|
31
|
+
// range: a range's minimum says nothing about the variant the buyer picks, and a ribbon is a
|
|
32
|
+
// label, not a price claim.
|
|
33
|
+
const ribbons = [product?.ribbon?.name, ...(product?.additionalRibbons ?? []).map((r) => r?.name)]
|
|
34
|
+
.filter((name, i, all) => name && all.indexOf(name) === i);
|
|
35
|
+
const promoBadge = ribbons[0] ? { type: "ribbon", label: ribbons[0] } : null;
|
|
44
36
|
|
|
45
|
-
// Price
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
37
|
+
// Price. A single-price product shows the price the buyer pays — the lowest-priced variant's
|
|
38
|
+
// discounted price when an automatic discount applies (priceAfterDiscount), else its regular
|
|
39
|
+
// price — with the struck "was" price beside it. Variants priced differently show a min–max
|
|
40
|
+
// range with NO struck price: one lone "was" against a range implies a saving that may not
|
|
41
|
+
// apply to the variant the buyer picks; the PDP shows the real comparison once a variant is chosen.
|
|
42
|
+
const min = product?.actualPriceRange?.minValue;
|
|
43
|
+
const max = product?.actualPriceRange?.maxValue;
|
|
44
|
+
const isRange = !!(min?.amount && max?.amount && min.amount !== max.amount);
|
|
45
|
+
const minVariant = product?.variantSummary?.minPriceVariant ?? null;
|
|
46
|
+
const { current, original } = sellingPrice(minVariant?.price);
|
|
47
|
+
const priceDisplay = isRange
|
|
48
|
+
? `${min?.formattedAmount ?? ""} – ${max?.formattedAmount ?? ""}`
|
|
49
|
+
: current?.formattedAmount ?? min?.formattedAmount;
|
|
50
|
+
const compareAtDisplay =
|
|
51
|
+
!isRange && original?.formattedAmount && Number(original.amount) > Number(current?.amount ?? min?.amount)
|
|
52
|
+
? original.formattedAmount
|
|
53
|
+
: null;
|
|
52
54
|
|
|
53
55
|
// Options preview for the tile summary row.
|
|
54
56
|
// Colour options → real hex dots (more informative than "3 colours").
|
|
@@ -68,6 +70,8 @@ export function useProductCard(product) {
|
|
|
68
70
|
// Quick-add is only safe for single-variant products (no option choices to resolve).
|
|
69
71
|
// Sold-out with pre-order still shows a CTA, but it links to the PDP, not quick-add.
|
|
70
72
|
const isQuickAddable = !hasOptions && !isSoldOut;
|
|
73
|
+
// The variant a direct add sends (a product with no options still has one variant).
|
|
74
|
+
const directAddVariantId = isQuickAddable ? minVariant?.id ?? null : null;
|
|
71
75
|
|
|
72
76
|
// Images: normalised through lib/storeImage so URLs are consistent across the tile,
|
|
73
77
|
// the PDP gallery, and the cart. Hover image is the second gallery shot (if one exists).
|
|
@@ -78,15 +82,17 @@ export function useProductCard(product) {
|
|
|
78
82
|
isSoldOut,
|
|
79
83
|
isPreorder,
|
|
80
84
|
isPartiallyOutOfStock,
|
|
81
|
-
leftBadges,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
|
|
86
|
+
ribbons, // every merchant ribbon label, primary first — render all of them
|
|
87
|
+
promoBadge, // { type: 'ribbon', label } | null — the primary ribbon, for a single-badge slot
|
|
88
|
+
priceDisplay, // formatted price the buyer pays, or a min–max range
|
|
89
|
+
compareAtDisplay, // formatted struck "was" price | null (never beside a range)
|
|
90
|
+
colors, // hex strings — render as dots; the tile shows up to however many you want
|
|
91
|
+
optionLabel, // "3 sizes · 2 materials" or empty string
|
|
87
92
|
isQuickAddable,
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
directAddVariantId, // pass as variantId to addToCart on quick-add
|
|
94
|
+
image, // primary image URL | null
|
|
95
|
+
hoverImage, // second image URL for hover effect | null
|
|
90
96
|
};
|
|
91
97
|
}, [product]);
|
|
92
98
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// useProductDetail — all PDP logic, no markup: load product by slug,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// renders what this returns.
|
|
1
|
+
// useProductDetail — all PDP logic, no markup: load product by slug, resolve the buyer's selections
|
|
2
|
+
// to a Wix variant, gate add-to-cart on availability + mandatory modifiers, and build the addToCart
|
|
3
|
+
// payload. The data paths here (variant resolution, price precedence, modifier keys, stock gating)
|
|
4
|
+
// are the bug-prone part — keep them verbatim; the PDP page only renders what this returns.
|
|
6
5
|
import { useState, useEffect, useMemo } from "react";
|
|
7
|
-
import { getProductBySlug } from "@/rest/wix-store-catalog";
|
|
6
|
+
import { getProductBySlug, sellingPrice } from "@/rest/wix-store-catalog";
|
|
8
7
|
import { choiceImage, variantImage } from "@/lib/storeImage";
|
|
9
8
|
import { useCart } from "@/context/CartContext";
|
|
10
9
|
|
|
10
|
+
// A variant is identified by the SET of its choice ids — sorted, so option order can't matter.
|
|
11
|
+
const choiceKey = (choiceIds) => [...choiceIds].sort().join("_");
|
|
12
|
+
|
|
11
13
|
export function useProductDetail(slug) {
|
|
12
14
|
const { addToCart } = useCart();
|
|
13
15
|
const [product, setProduct] = useState(null);
|
|
@@ -16,6 +18,8 @@ export function useProductDetail(slug) {
|
|
|
16
18
|
// link, the other is worth retrying. Without the split, a network blip reads as "product deleted".
|
|
17
19
|
const [error, setError] = useState(null);
|
|
18
20
|
const [adding, setAdding] = useState(false);
|
|
21
|
+
// Selections start EMPTY. Pre-picking the first choice lets a buyer who never noticed the default
|
|
22
|
+
// buy the wrong colour; the PDP shows a neutral "choose a size" until every option is picked.
|
|
19
23
|
const [selectedOptions, setSelectedOptions] = useState({});
|
|
20
24
|
const [modifierValues, setModifierValues] = useState({});
|
|
21
25
|
const [quantity, setQuantity] = useState(1);
|
|
@@ -26,17 +30,13 @@ export function useProductDetail(slug) {
|
|
|
26
30
|
setProduct(null);
|
|
27
31
|
setNotFound(false);
|
|
28
32
|
setError(null);
|
|
33
|
+
setSelectedOptions({});
|
|
34
|
+
setQuantity(1);
|
|
29
35
|
getProductBySlug(slug)
|
|
30
36
|
.then((p) => {
|
|
31
37
|
if (cancelled) return;
|
|
32
38
|
if (!p) return setNotFound(true);
|
|
33
39
|
setProduct(p);
|
|
34
|
-
const initial = {};
|
|
35
|
-
(p.options || []).forEach((o) => {
|
|
36
|
-
const first = o.choicesSettings?.choices?.find((c) => c.inStock !== false);
|
|
37
|
-
if (first) initial[o.id] = first.choiceId;
|
|
38
|
-
});
|
|
39
|
-
setSelectedOptions(initial);
|
|
40
40
|
})
|
|
41
41
|
.catch((e) => { if (!cancelled) setError(e?.message || "Couldn't load this product."); });
|
|
42
42
|
return () => { cancelled = true; };
|
|
@@ -44,29 +44,70 @@ export function useProductDetail(slug) {
|
|
|
44
44
|
|
|
45
45
|
const options = product?.options || [];
|
|
46
46
|
const modifiers = product?.modifiers || [];
|
|
47
|
-
const variants = product?.variantsInfo?.variants || [];
|
|
48
47
|
|
|
48
|
+
// Visible variants keyed by their sorted choice ids. Two variants with the same choices is a
|
|
49
|
+
// catalog defect — report it instead of silently letting one overwrite the other.
|
|
50
|
+
const { variantByKey, duplicateKey } = useMemo(() => {
|
|
51
|
+
const map = new Map();
|
|
52
|
+
let dup = null;
|
|
53
|
+
for (const v of product?.variantsInfo?.variants || []) {
|
|
54
|
+
if (v.visible === false) continue;
|
|
55
|
+
const k = choiceKey((v.choices || []).map((c) => c.optionChoiceIds?.choiceId));
|
|
56
|
+
if (map.has(k) && !dup) dup = k;
|
|
57
|
+
map.set(k, v);
|
|
58
|
+
}
|
|
59
|
+
return { variantByKey: map, duplicateKey: dup };
|
|
60
|
+
}, [product]);
|
|
61
|
+
|
|
62
|
+
const selectionComplete = options.length > 0 && options.every((o) => selectedOptions[o.id]);
|
|
49
63
|
const variant = useMemo(() => {
|
|
50
|
-
if (options.length === 0) return
|
|
51
|
-
if (!
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
}, [options, variants, selectedOptions]);
|
|
64
|
+
if (options.length === 0) return variantByKey.values().next().value || null; // the single variant
|
|
65
|
+
if (!selectionComplete || duplicateKey) return null;
|
|
66
|
+
return variantByKey.get(choiceKey(options.map((o) => selectedOptions[o.id]))) || null;
|
|
67
|
+
}, [options, variantByKey, duplicateKey, selectedOptions, selectionComplete]);
|
|
55
68
|
|
|
69
|
+
// In stock OR pre-orderable counts as buyable; a preorder add carries preOrderRequested.
|
|
56
70
|
const inStock = variant ? variant.inventoryStatus?.inStock !== false : true;
|
|
71
|
+
const isPreorder = !!variant && !inStock && variant.inventoryStatus?.preorderEnabled === true;
|
|
72
|
+
const available = inStock || isPreorder;
|
|
73
|
+
|
|
74
|
+
const missingModifier = modifiers.find((m) => m.mandatory && !(
|
|
75
|
+
m.modifierRenderType === "FREE_TEXT" ? modifierValues[m.freeTextSettings?.key] : modifierValues[m.key]));
|
|
76
|
+
|
|
57
77
|
const canAdd = useMemo(() => {
|
|
78
|
+
if (!product) return false;
|
|
58
79
|
if (options.length > 0 && !variant) return false;
|
|
59
|
-
if (variant && !
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
if (variant && !available) return false;
|
|
81
|
+
return !missingModifier;
|
|
82
|
+
}, [product, options, variant, available, missingModifier]);
|
|
83
|
+
|
|
84
|
+
// Why the buy button is disabled — neutral guidance to render next to the action ("Choose a
|
|
85
|
+
// size"), not an error; null when the buyer can add. Promote to error styling only after they try.
|
|
86
|
+
const blockedReason = useMemo(() => {
|
|
87
|
+
if (!product) return null;
|
|
88
|
+
if (duplicateKey) return "This product's variants can't be resolved right now.";
|
|
89
|
+
if (options.length > 0 && !selectionComplete) {
|
|
90
|
+
const missing = options.filter((o) => !selectedOptions[o.id]).map((o) => o.name);
|
|
91
|
+
return `Choose ${missing.join(" and ")}`;
|
|
92
|
+
}
|
|
93
|
+
if (options.length > 0 && !variant) return "This combination isn't available";
|
|
94
|
+
if (variant && !available) return "Out of stock";
|
|
95
|
+
if (missingModifier) return `Add ${missingModifier.name}`;
|
|
96
|
+
return null;
|
|
97
|
+
}, [product, duplicateKey, options, selectionComplete, selectedOptions, variant, available, missingModifier]);
|
|
63
98
|
|
|
64
|
-
// Everything the buyer sees follows the resolved `variant` (also returned below
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
const
|
|
99
|
+
// Everything the buyer sees follows the resolved `variant` (also returned below). The price obeys
|
|
100
|
+
// the exact precedence in sellingPrice(): an automatic discount beats the regular price and strikes
|
|
101
|
+
// it; else the regular price with the merchant's compare-at as the "was". Before every option is
|
|
102
|
+
// picked, the product's min–max range (one value when equal) — never an empty price.
|
|
103
|
+
const min = product?.actualPriceRange?.minValue, max = product?.actualPriceRange?.maxValue;
|
|
104
|
+
const rangeDisplay = min?.amount && max?.amount && min.amount !== max.amount
|
|
105
|
+
? `${min.formattedAmount} – ${max.formattedAmount}` : (min?.formattedAmount || "");
|
|
106
|
+
const resolved = variant ? sellingPrice(variant.price) : null;
|
|
107
|
+
const price = resolved?.current?.formattedAmount || rangeDisplay;
|
|
108
|
+
const compareAtPrice = resolved && resolved.original?.formattedAmount &&
|
|
109
|
+
Number(resolved.original.amount) > Number(resolved.current?.amount) ? resolved.original.formattedAmount : "";
|
|
110
|
+
const discountNames = product?.discountInfo?.discountRuleNames || []; // automatic discounts, product level
|
|
70
111
|
|
|
71
112
|
// The gallery follows the selection. Prefer the selected choice's own image (the per-swatch photo,
|
|
72
113
|
// choice.media.items[].mediaId) — Wix's per-variant media is often NOT differentiated (it defaults to
|
|
@@ -82,7 +123,11 @@ export function useProductDetail(slug) {
|
|
|
82
123
|
return variantImage(variant);
|
|
83
124
|
}, [variant, options, selectedOptions]);
|
|
84
125
|
|
|
85
|
-
|
|
126
|
+
// Changing an option resets the quantity — the ceiling belongs to the newly resolved variant.
|
|
127
|
+
const selectOption = (optionId, choiceId) => {
|
|
128
|
+
setSelectedOptions((s) => ({ ...s, [optionId]: choiceId }));
|
|
129
|
+
setQuantity(1);
|
|
130
|
+
};
|
|
86
131
|
const setModifier = (key, value) => setModifierValues((s) => ({ ...s, [key]: value }));
|
|
87
132
|
|
|
88
133
|
async function submit() {
|
|
@@ -101,6 +146,7 @@ export function useProductDetail(slug) {
|
|
|
101
146
|
return await addToCart(product.id, variant?.id, Math.max(1, Number(quantity) || 1), {
|
|
102
147
|
modifierChoices: Object.keys(modifierChoices).length ? modifierChoices : undefined,
|
|
103
148
|
customTextFields: Object.keys(customTextFields).length ? customTextFields : undefined,
|
|
149
|
+
preorder: isPreorder,
|
|
104
150
|
});
|
|
105
151
|
} finally {
|
|
106
152
|
setAdding(false);
|
|
@@ -111,6 +157,7 @@ export function useProductDetail(slug) {
|
|
|
111
157
|
product, notFound, error, retry: () => setReloadKey((k) => k + 1),
|
|
112
158
|
options, modifiers,
|
|
113
159
|
selectedOptions, selectOption, modifierValues, setModifier,
|
|
114
|
-
quantity, setQuantity, variant, inStock,
|
|
160
|
+
quantity, setQuantity, variant, inStock, isPreorder, canAdd, blockedReason, adding,
|
|
161
|
+
price, compareAtPrice, discountNames, submit, focusMediaUrl,
|
|
115
162
|
};
|
|
116
163
|
}
|
|
@@ -40,17 +40,24 @@ const STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
|
40
40
|
* Throws on out-of-stock so the buyer can't reach checkout with an unbuyable line.
|
|
41
41
|
* Full catalogReference reference: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/e-commerce-integration.md
|
|
42
42
|
*
|
|
43
|
+
* A pre-order variant (inventoryStatus.preorderEnabled, out of stock) is added with `preorder: true`;
|
|
44
|
+
* a recurring subscription plan with its `subscriptionOptionId` (omit it for a one-time purchase).
|
|
45
|
+
* The two may coexist.
|
|
46
|
+
*
|
|
43
47
|
* @param {string} catalogItemId Product GUID (product.id).
|
|
44
48
|
* @param {string} [variantId] variantsInfo.variants[].id — required for products with variants.
|
|
45
49
|
* @param {number} [quantity]
|
|
46
|
-
* @param {{ modifierChoices?: Record<string,string>, customTextFields?: Record<string,string
|
|
50
|
+
* @param {{ modifierChoices?: Record<string,string>, customTextFields?: Record<string,string>,
|
|
51
|
+
* subscriptionOptionId?: string, preorder?: boolean }} [extras]
|
|
47
52
|
* @returns {Promise<object>} Updated cart.
|
|
48
53
|
*/
|
|
49
|
-
export async function addToCart(catalogItemId, variantId, quantity = 1, { modifierChoices, customTextFields } = {}) {
|
|
54
|
+
export async function addToCart(catalogItemId, variantId, quantity = 1, { modifierChoices, customTextFields, subscriptionOptionId, preorder } = {}) {
|
|
50
55
|
const catalogReferenceOptions = {};
|
|
51
56
|
if (variantId) catalogReferenceOptions.variantId = variantId;
|
|
52
57
|
if (modifierChoices && Object.keys(modifierChoices).length) catalogReferenceOptions.options = modifierChoices;
|
|
53
58
|
if (customTextFields && Object.keys(customTextFields).length) catalogReferenceOptions.customTextFields = customTextFields;
|
|
59
|
+
if (subscriptionOptionId) catalogReferenceOptions.subscriptionOptionId = subscriptionOptionId;
|
|
60
|
+
if (preorder) catalogReferenceOptions.preOrderRequested = true;
|
|
54
61
|
|
|
55
62
|
const catalogReference = { appId: STORES_APP_ID, catalogItemId };
|
|
56
63
|
if (Object.keys(catalogReferenceOptions).length) catalogReference.options = catalogReferenceOptions;
|
|
@@ -92,8 +99,30 @@ export async function getCurrentCart() {
|
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Estimate the current cart's totals — the ONLY source of the after-discount subtotal and the
|
|
104
|
+
* cart-level discount. Line prices already carry item-level discounts; delivery and tax resolve at
|
|
105
|
+
* checkout unless asked for, so never invent them. Returns null when there is no cart or the
|
|
106
|
+
* estimate fails (a display nicety — the cart itself stays valid).
|
|
107
|
+
* https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/estimate-current-cart.md
|
|
108
|
+
* @returns {Promise<{ priceSummary: { subtotal: object, discount: object, total: object } }|null>}
|
|
109
|
+
* priceSummary amounts are ConvertedMoney { amount, convertedAmount } — format like line prices.
|
|
110
|
+
*/
|
|
111
|
+
export async function estimateCurrentCart() {
|
|
112
|
+
try {
|
|
113
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/estimate", { method: "POST", body: {} });
|
|
114
|
+
return res?.summary ?? null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
/**
|
|
96
121
|
* Start the hosted checkout for the current cart and return its URL.
|
|
122
|
+
* This is the shopper's CURRENT cart. A "Buy now" that skips the cart needs a standalone cart
|
|
123
|
+
* (POST /ecom/v2/carts) handed to the redirect session — it must never append to or replace the
|
|
124
|
+
* current cart. Not shipped here; verify the redirect session accepts a standalone cart id under
|
|
125
|
+
* the visitor token before adding it.
|
|
97
126
|
* In Cart V2 the cart id IS the checkout id — there is no separate checkout-creation call; the
|
|
98
127
|
* redirect session is created straight from the current cart's id.
|
|
99
128
|
* Throws on empty cart, unavailable lines, or a missing redirect URL.
|
|
@@ -13,8 +13,13 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
13
13
|
* actualPriceRange.minValue.formattedAmount {string} — lowest price with currency symbol,
|
|
14
14
|
* actualPriceRange.maxValue.formattedAmount {string} — highest price with currency symbol,
|
|
15
15
|
* compareAtPriceRange.minValue.formattedAmount {string} — strikethrough price (present when on sale),
|
|
16
|
-
* actualPriceRange/compareAtPriceRange .minValue.amount {string} — the same figures unformatted
|
|
17
|
-
*
|
|
16
|
+
* actualPriceRange/compareAtPriceRange .minValue.amount {string} — the same figures unformatted.
|
|
17
|
+
* Never compute a percent-off or "you save" claim from them: a range's minimum says nothing about
|
|
18
|
+
* the variant the buyer picks. The price the buyer pays comes from sellingPrice() below.
|
|
19
|
+
* variantSummary.minPriceVariant {object} — the lowest-priced visible variant (MIN_PRICE_VARIANT):
|
|
20
|
+
* { id, price: { actualPrice, compareAtPrice?, priceAfterDiscount? } } — a card's price, and the
|
|
21
|
+
* variantId for a direct add on a product with no options,
|
|
22
|
+
* discountInfo.discountRuleNames {string[]} — automatic-discount names (DISCOUNT_INFO), product level,
|
|
18
23
|
* inventory.availabilityStatus {string} — "IN_STOCK"|"OUT_OF_STOCK"|"PARTIALLY_OUT_OF_STOCK",
|
|
19
24
|
* inventory.preorderStatus {string} — "ENABLED"|"DISABLED". ENABLED + OUT_OF_STOCK is a pre-order,
|
|
20
25
|
* which is buyable — label it "Pre-order" rather than "Sold out",
|
|
@@ -23,8 +28,9 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
23
28
|
* getProductBySlug call per product,
|
|
24
29
|
* ribbon {object} — { id, name } merchant-set badge: "New", "Sale", "Best Seller". This is the
|
|
25
30
|
* catalogue's own label, so prefer it over anything derived — a "new" badge computed from
|
|
26
|
-
* createdDate flags every product at once right after seeding
|
|
27
|
-
*
|
|
31
|
+
* createdDate flags every product at once right after seeding. A ribbon is a LABEL: a "Sale"
|
|
32
|
+
* ribbon is not proof of a lower price, and a compareAtPrice does not earn a ribbon,
|
|
33
|
+
* additionalRibbons {array} — up to four further { id, name } badges. Render ALL of them,
|
|
28
34
|
* createdDate / updatedDate {string} — ISO timestamps,
|
|
29
35
|
* variantSummary.variantCount {number} — how many variants exist, without fetching them,
|
|
30
36
|
* options {array} — product options e.g. Size, Color:
|
|
@@ -45,9 +51,11 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
45
51
|
* dangerouslySetInnerHTML; strip tags only for plain-text contexts (meta description, teaser),
|
|
46
52
|
* variantsInfo.variants {array} — returned only by getProductBySlug:
|
|
47
53
|
* [{ id, visible, choices [{ optionChoiceIds: { optionId, choiceId } }],
|
|
48
|
-
* price: { actualPrice, compareAtPrice }, media,
|
|
54
|
+
* price: { actualPrice, compareAtPrice?, priceAfterDiscount? }, media,
|
|
55
|
+
* inventoryStatus: { inStock, preorderEnabled } }]
|
|
49
56
|
* To resolve a buyer's option selections to a variantId: find the variant whose choices
|
|
50
57
|
* match all selected { optionId, choiceId } pairs, then pass variant.id to addToCart.
|
|
58
|
+
* A variant is buyable when inStock OR preorderEnabled (a preorder add carries preOrderRequested).
|
|
51
59
|
*
|
|
52
60
|
* Category: { id, name, slug, visible, description, image, itemCounter, parentCategory.id }
|
|
53
61
|
* NB: queryCategories includes the auto-created system category { slug: "all-products" } —
|
|
@@ -55,6 +63,20 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
55
63
|
* Full model: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/categories
|
|
56
64
|
*/
|
|
57
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The price the buyer pays, and the price to strike — exact precedence, shared by cards and the PDP.
|
|
68
|
+
* An automatic discount (priceAfterDiscount, present only when DISCOUNT_INFO is requested) wins and
|
|
69
|
+
* strikes the regular actualPrice; otherwise actualPrice with the merchant's compareAtPrice as the
|
|
70
|
+
* "was". `!== undefined` on purpose: a discounted price of 0 is a real price.
|
|
71
|
+
* @param {{ actualPrice?: object, compareAtPrice?: object, priceAfterDiscount?: object }} [price]
|
|
72
|
+
* variant.price, or variantSummary.minPriceVariant.price. Each is { amount, formattedAmount }.
|
|
73
|
+
* @returns {{ current: object|undefined, original: object|undefined }} render .formattedAmount
|
|
74
|
+
*/
|
|
75
|
+
export function sellingPrice(price) {
|
|
76
|
+
if (price?.priceAfterDiscount !== undefined) return { current: price.priceAfterDiscount, original: price.actualPrice };
|
|
77
|
+
return { current: price?.actualPrice, original: price?.compareAtPrice };
|
|
78
|
+
}
|
|
79
|
+
|
|
58
80
|
// Search Products supports server-side sort/filter before cursor paging:
|
|
59
81
|
// https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md
|
|
60
82
|
export const CATALOG_SORTS = {
|
|
@@ -108,7 +130,9 @@ export async function searchProducts({ limit = 100, cursor, categoryId, sort = "
|
|
|
108
130
|
}
|
|
109
131
|
const res = await wixApiRequest("/stores/v3/products/search", {
|
|
110
132
|
method: "POST",
|
|
111
|
-
|
|
133
|
+
// MIN_PRICE_VARIANT + DISCOUNT_INFO → variantSummary.minPriceVariant with its discounted price:
|
|
134
|
+
// the card's price and the direct-add variantId, without a per-product fetch.
|
|
135
|
+
body: { fields: ["CURRENCY", "PLAIN_DESCRIPTION", "MEDIA_ITEMS_INFO", "MIN_PRICE_VARIANT", "DISCOUNT_INFO"], search: query },
|
|
112
136
|
});
|
|
113
137
|
return { products: res?.products ?? [], nextCursor: res?.pagingMetadata?.cursors?.next ?? null };
|
|
114
138
|
}
|
|
@@ -137,6 +161,7 @@ export async function getProductBySlug(slug) {
|
|
|
137
161
|
"MEDIA_ITEMS_INFO",
|
|
138
162
|
"PRODUCT_CHOICES_MEDIA_REFERENCES",
|
|
139
163
|
"VARIANT_OPTION_CHOICE_NAMES",
|
|
164
|
+
"DISCOUNT_INFO", // variant price.priceAfterDiscount + discountInfo.discountRuleNames
|
|
140
165
|
],
|
|
141
166
|
},
|
|
142
167
|
});
|