@wix/agent-skills 1.16.2 → 1.17.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/package.json +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +134 -120
- package/skills/wix-app/references/EMBEDDED_SCRIPT.md +6 -0
- package/skills/wix-app/references/SITE_PLUGIN.md +2 -2
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +2 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +182 -99
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +92 -47
- package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +15 -6
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +275 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +29 -42
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +153 -449
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +66 -19
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +53 -158
- package/skills/wix-app/references/editor-react-component/EDITOR-EXTENSION-CONFIGURATION.md +78 -0
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +42 -134
- package/skills/wix-app/references/editor-react-component/MANIFEST-ERRORS.md +27 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +45 -24
- package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +63 -179
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +79 -163
- package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +36 -21
- package/skills/wix-app/references/editor-react-component/SSR.md +30 -99
- package/skills/wix-base44-connector/SKILL.md +34 -17
- package/skills/wix-base44-connector/scripts/utils.js +33 -25
- package/skills/wix-headless/entry/bootstrap.mjs +9 -2
- package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +42 -23
- package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +10 -8
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +27 -24
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +5 -0
- package/skills/wix-manage/SKILL.md +14 -2
- package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +20 -13
- package/skills/wix-manage/references/bookings/create-class-service.md +1 -1
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +24 -11
- package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +5 -5
- package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +182 -5
- package/skills/wix-manage/references/seo/manage-seo-tags.md +315 -0
- package/skills/wix-manage/references/seo/manage-url-redirects.md +250 -0
- package/skills/wix-manage/references/sites/query-sites.md +72 -161
- package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +5 -6
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +10 -7
- package/skills/wix-vibe-headless/platforms/base44.md +13 -10
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +2 -0
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-checkout.js +9 -8
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +4 -1
- package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartDrawer.jsx +23 -11
- package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +29 -31
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +15 -8
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +37 -38
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +2 -2
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +35 -35
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW-REFERENCE.md +0 -172
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +0 -143
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +0 -484
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +0 -225
- package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +0 -6
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +0 -95
package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js
CHANGED
|
@@ -11,15 +11,16 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
11
11
|
* fulfillmentIds {string[]}, businessLocationId {string}
|
|
12
12
|
* Full model: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/operation-object.md
|
|
13
13
|
*
|
|
14
|
-
* Cart (Wix eCom) — restaurant orders use the same visitor cart as the storefront.
|
|
15
|
-
* id {string}
|
|
14
|
+
* Cart (Wix eCom V2) — restaurant orders use the same visitor cart as the storefront.
|
|
15
|
+
* id {string} — the cart id IS the checkout id,
|
|
16
16
|
* lineItems[].id {string} — lineItemId for update/remove (NOT the item id),
|
|
17
|
-
* lineItems[].
|
|
18
|
-
* lineItems[].catalogReference.catalogItemId {string} — the ordered item's GUID,
|
|
19
|
-
* lineItems[].
|
|
20
|
-
* lineItems[].
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* lineItems[].quantityInfo.confirmedQuantity {number},
|
|
18
|
+
* lineItems[].source.catalogReference.catalogItemId {string} — the ordered item's GUID (NOT top-level in V2),
|
|
19
|
+
* lineItems[].name.original {string},
|
|
20
|
+
* lineItems[].pricing.unitPrice / lineItems[].pricing.totalPrice {ConvertedMoney} —
|
|
21
|
+
* { amount, convertedAmount } with NO formatted string; format the number client-side,
|
|
22
|
+
* lineItems[].status {string} — "IN_STOCK"|"PARTIALLY_IN_STOCK"|"OUT_OF_STOCK"|"REMOVED_FROM_CATALOG"
|
|
23
|
+
* Full model: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/get-current-cart.md
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
// Wix Restaurants Orders app id — required in catalogReference when adding menu items to the eCom cart.
|
|
@@ -61,7 +62,7 @@ export async function getDefaultOperation() {
|
|
|
61
62
|
* Confirm the shape before extending:
|
|
62
63
|
* https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/sample-flows.md
|
|
63
64
|
*
|
|
64
|
-
* POST https://www.wixapis.com/ecom/
|
|
65
|
+
* POST https://www.wixapis.com/ecom/v2/carts/current/add-line-items
|
|
65
66
|
* @param {string} itemId Menu item GUID (item.id).
|
|
66
67
|
* @param {{ operationId: string, menuId: string, sectionId: string, onlineOrderingPageUrl?: string, quantity?: number }} opts
|
|
67
68
|
* @returns {Promise<object>} Updated cart.
|
|
@@ -74,15 +75,16 @@ export async function addItemToCart(itemId, { operationId, menuId, sectionId, on
|
|
|
74
75
|
const options = { operationId, menuId, sectionId };
|
|
75
76
|
if (onlineOrderingPageUrl) options.onlineOrderingPageUrl = onlineOrderingPageUrl;
|
|
76
77
|
|
|
77
|
-
const res = await wixApiRequest("/ecom/
|
|
78
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/add-line-items", {
|
|
78
79
|
method: "POST",
|
|
79
80
|
body: {
|
|
80
|
-
|
|
81
|
+
catalogItems: [{ catalogReference: { appId: RESTAURANTS_ORDERS_APP_ID, catalogItemId: itemId, options }, quantity }],
|
|
81
82
|
},
|
|
82
83
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// V2 nests the reference under `source` — a top-level lineItem.catalogReference no longer exists.
|
|
85
|
+
const line = (res?.cart?.lineItems ?? []).find((l) => l.source?.catalogReference?.catalogItemId === itemId);
|
|
86
|
+
if (line?.status && line.status !== "IN_STOCK") {
|
|
87
|
+
throw new Error(`Item not available to order (status: ${line.status}).`);
|
|
86
88
|
}
|
|
87
89
|
return res?.cart;
|
|
88
90
|
}
|
|
@@ -90,7 +92,7 @@ export async function addItemToCart(itemId, { operationId, menuId, sectionId, on
|
|
|
90
92
|
/** Read the visitor's current cart. Returns null if no cart exists yet. */
|
|
91
93
|
export async function getCurrentCart() {
|
|
92
94
|
try {
|
|
93
|
-
const res = await wixApiRequest("/ecom/
|
|
95
|
+
const res = await wixApiRequest("/ecom/v2/carts/current", { method: "GET" });
|
|
94
96
|
return res?.cart ?? null;
|
|
95
97
|
} catch {
|
|
96
98
|
return null;
|
|
@@ -99,24 +101,24 @@ export async function getCurrentCart() {
|
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
103
|
* Update the quantity of a cart line. lineItemId is cart.lineItems[].id, not the item id.
|
|
102
|
-
* POST https://www.wixapis.com/ecom/
|
|
104
|
+
* POST https://www.wixapis.com/ecom/v2/carts/current/update-line-items
|
|
103
105
|
* @returns {Promise<object>} Updated cart.
|
|
104
106
|
*/
|
|
105
107
|
export async function updateCartItemQuantity(lineItemId, quantity) {
|
|
106
|
-
const res = await wixApiRequest("/ecom/
|
|
108
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/update-line-items", {
|
|
107
109
|
method: "POST",
|
|
108
|
-
body: { lineItems: [{
|
|
110
|
+
body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] },
|
|
109
111
|
});
|
|
110
112
|
return res?.cart;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Remove a line from the current cart by its cart.lineItems[].id.
|
|
115
|
-
* POST https://www.wixapis.com/ecom/
|
|
117
|
+
* POST https://www.wixapis.com/ecom/v2/carts/current/remove-line-items
|
|
116
118
|
* @returns {Promise<object>} Updated cart.
|
|
117
119
|
*/
|
|
118
120
|
export async function removeFromCart(lineItemId) {
|
|
119
|
-
const res = await wixApiRequest("/ecom/
|
|
121
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/remove-line-items", {
|
|
120
122
|
method: "POST",
|
|
121
123
|
body: { lineItemIds: [lineItemId] },
|
|
122
124
|
});
|
|
@@ -124,7 +126,9 @@ export async function removeFromCart(lineItemId) {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
/**
|
|
127
|
-
*
|
|
129
|
+
* Start the hosted checkout for the current cart and return the Wix-hosted checkout URL.
|
|
130
|
+
* In Cart V2 the cart id IS the checkout id — there is no separate checkout-creation call; the
|
|
131
|
+
* redirect session is created straight from the current cart's id.
|
|
128
132
|
* Throws on empty cart, unavailable lines, or a missing redirect URL.
|
|
129
133
|
* Redirect with: window.location.href = await checkout()
|
|
130
134
|
* @returns {Promise<string>}
|
|
@@ -133,22 +137,16 @@ export async function checkout() {
|
|
|
133
137
|
const cart = await getCurrentCart();
|
|
134
138
|
const lines = cart?.lineItems ?? [];
|
|
135
139
|
if (!lines.length) throw new Error("Cannot check out: the cart is empty.");
|
|
136
|
-
const unavailable = lines.filter((l) => l.
|
|
140
|
+
const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
|
|
137
141
|
if (unavailable.length) {
|
|
138
|
-
const names = unavailable.map((l) => l.
|
|
142
|
+
const names = unavailable.map((l) => l.name?.original ?? l.source?.catalogReference?.catalogItemId).join(", ");
|
|
139
143
|
throw new Error(`Cannot check out: ${unavailable.length} item(s) not available — ${names}.`);
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
const checkoutRes = await wixApiRequest("/ecom/v1/carts/current/create-checkout", {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body: { channelType: "WEB" },
|
|
145
|
-
});
|
|
146
|
-
const checkoutId = checkoutRes?.checkoutId;
|
|
147
|
-
if (!checkoutId) throw new Error("Failed to create checkout from the current cart.");
|
|
145
|
+
if (!cart.id) throw new Error("Failed to check out: the current cart has no id.");
|
|
148
146
|
|
|
149
147
|
const redirect = await wixApiRequest("/headless/v1/redirect-session", {
|
|
150
148
|
method: "POST",
|
|
151
|
-
body: { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl: window.location.href } },
|
|
149
|
+
body: { ecomCheckout: { checkoutId: cart.id }, callbacks: { postFlowUrl: window.location.href } },
|
|
152
150
|
});
|
|
153
151
|
const url = redirect?.redirectSession?.fullUrl;
|
|
154
152
|
if (!url) throw new Error("Failed to create the checkout redirect session.");
|
|
@@ -237,6 +237,8 @@ render.
|
|
|
237
237
|
|
|
238
238
|
## Using the client from your own UI (cart, hand-built images)
|
|
239
239
|
|
|
240
|
+
> Migrating from Cart V1 / Checkout V1? These helpers are V2-only — see the [migration guide](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/migration-guide) for the before/after.
|
|
241
|
+
|
|
240
242
|
```jsx
|
|
241
243
|
import { Link } from "react-router-dom";
|
|
242
244
|
import { useCart } from "@/context/CartContext";
|
|
@@ -246,15 +248,20 @@ import { useCart } from "@/context/CartContext";
|
|
|
246
248
|
// addToCart(productId, variantId?, qty=1, { modifierChoices?, customTextFields? }?),
|
|
247
249
|
// removeItem(lineItemId), updateQuantity(lineItemId, qty), checkout(), refreshCart() }
|
|
248
250
|
// Every mutation catches its own failure into `error` (the shipped CartDrawer renders it), so a
|
|
249
|
-
// refusal — an empty cart, or a line item
|
|
251
|
+
// refusal — an empty cart, or a line item whose status left IN_STOCK — reaches the buyer instead
|
|
250
252
|
// of becoming an unhandled rejection.
|
|
251
253
|
//
|
|
252
|
-
// Cart money: every amount is { amount, convertedAmount
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// `
|
|
254
|
+
// Cart money: every amount is a ConvertedMoney { amount, convertedAmount } with NO formatted string —
|
|
255
|
+
// `amount` is in the site currency, `convertedAmount` in the display currency; format the number
|
|
256
|
+
// yourself (Intl.NumberFormat with the cart's currency —
|
|
257
|
+
// `cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode`) so the symbol and grouping
|
|
258
|
+
// match the locale. The V2 cart has no `subtotalAfterDiscounts`/`discount`/`appliedDiscounts`: it
|
|
259
|
+
// carries only a raw `cart.subtotal` (ConvertedMoney). The authoritative discounted totals come from
|
|
260
|
+
// a currentCartV2 estimate/calculate `summary.priceSummary`, not the cart — call that when you need
|
|
261
|
+
// the after-coupon figure; otherwise show `cart.subtotal`. Never sum line items yourself — tax and
|
|
262
|
+
// shipping resolve at checkout.
|
|
263
|
+
// A line's `status` (IN_STOCK / PARTIALLY_IN_STOCK / OUT_OF_STOCK) tells a quantity control whether
|
|
264
|
+
// another increment is still fulfillable.
|
|
258
265
|
|
|
259
266
|
function CartCount() { // header badge
|
|
260
267
|
const { itemCount, setIsOpen } = useCart();
|
|
@@ -294,7 +301,7 @@ const { products: inCategory } = await queryProductsByCategory(menu[0].id, { lim
|
|
|
294
301
|
|
|
295
302
|
// product.plainDescription is HTML → render as HTML (the PDP does this):
|
|
296
303
|
<div dangerouslySetInnerHTML={{ __html: product.plainDescription }} />
|
|
297
|
-
// image urls live at: product.media.main.image.url · cart lineItems[].image.url
|
|
304
|
+
// image urls live at: product.media.main.image.url · cart lineItems[].attributes.image.url
|
|
298
305
|
```
|
|
299
306
|
|
|
300
307
|
Fallback only — when you hit an error or need something not shown here (coupons, members, a field
|
|
@@ -2,29 +2,36 @@
|
|
|
2
2
|
// check out via the context (redirect-session URL). Styled with base44 design tokens (shadcn Tailwind classes).
|
|
3
3
|
//
|
|
4
4
|
// Load-bearing beyond the styling:
|
|
5
|
-
// • prices come from the SERVER cart
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// • `
|
|
5
|
+
// • prices come from the SERVER cart. In Cart V2 each line total is `pricing.totalPrice`, a
|
|
6
|
+
// ConvertedMoney { amount, convertedAmount } with NO formatted string — format it yourself with the
|
|
7
|
+
// cart's currency (see formatCartMoney). Never compute a total in the client: tax, shipping and
|
|
8
|
+
// promotions are resolved server-side, so the cart's `subtotal` is the only figure safe to show
|
|
9
|
+
// pre-checkout. The V2 cart has no `subtotalAfterDiscounts`/`discount`/`appliedDiscounts` — the
|
|
10
|
+
// discounted total comes from a currentCartV2 estimate/calculate `summary.priceSummary`, not the
|
|
11
|
+
// cart; absent that call, show the raw `subtotal`.
|
|
12
|
+
// • `quantityInfo.availableQuantity` caps the stepper, so exceeding stock is refused here rather than
|
|
13
13
|
// at checkout.
|
|
14
|
-
// • `
|
|
15
|
-
//
|
|
14
|
+
// • `status` is flagged per line here because checkout() refuses the whole cart when any item isn't
|
|
15
|
+
// IN_STOCK — showing it on the row is what makes that refusal understandable.
|
|
16
16
|
import { useEffect } from "react";
|
|
17
17
|
import { useCart } from "@/context/CartContext";
|
|
18
18
|
import { storeImage } from "@/lib/storeImage";
|
|
19
19
|
|
|
20
|
+
// Cart V2 money is a ConvertedMoney { amount, convertedAmount } with NO formatted string — format it
|
|
21
|
+
// with the cart's currency (buyer's display currency when present, else the site currency).
|
|
22
|
+
function formatCartMoney(money, cart) {
|
|
23
|
+
const value = money?.convertedAmount ?? money?.amount;
|
|
24
|
+
const currency = cart?.customerInfo?.currencyCode ?? cart?.businessInfo?.currencyCode ?? "USD";
|
|
25
|
+
return value == null ? "" : new Intl.NumberFormat(undefined, { style: "currency", currency }).format(Number(value));
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export default function CartDrawer() {
|
|
21
29
|
const { cart, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading, error, clearError } = useCart();
|
|
22
30
|
const lineItems = cart?.lineItems ?? [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const unavailable = lineItems.filter((li) => li.availability?.status && li.availability.status !== "AVAILABLE");
|
|
31
|
+
// The V2 cart carries only a raw `subtotal` (ConvertedMoney); discounted totals live on an
|
|
32
|
+
// estimate/calculate `summary.priceSummary`, which this cart helper doesn't fetch — so show `subtotal`.
|
|
33
|
+
const subtotal = formatCartMoney(cart?.subtotal, cart);
|
|
34
|
+
const unavailable = lineItems.filter((li) => li.status && li.status !== "IN_STOCK");
|
|
28
35
|
|
|
29
36
|
// Escape closes the drawer while it's open — expected of anything modal, and the only way out for
|
|
30
37
|
// keyboard users (the backdrop click is pointer-only).
|
|
@@ -70,46 +77,44 @@ export default function CartDrawer() {
|
|
|
70
77
|
className="border border-border bg-card text-foreground rounded-sm py-2 px-4 cursor-pointer text-sm">Continue shopping</button>
|
|
71
78
|
</div>
|
|
72
79
|
) : lineItems.map((item) => {
|
|
73
|
-
const image = storeImage(item.image?.url);
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const atStockLimit = Number.isFinite(stock) &&
|
|
80
|
+
const image = storeImage(item.attributes?.image?.url);
|
|
81
|
+
const paid = formatCartMoney(item.pricing?.totalPrice, cart);
|
|
82
|
+
const isOut = item.status && item.status !== "IN_STOCK";
|
|
83
|
+
const stock = item.quantityInfo?.availableQuantity;
|
|
84
|
+
const qty = item.quantityInfo?.confirmedQuantity;
|
|
85
|
+
const atStockLimit = Number.isFinite(stock) && qty >= stock;
|
|
79
86
|
return (
|
|
80
87
|
<div key={item.id} className="flex gap-3 py-3 px-0 border-b border-border">
|
|
81
88
|
<div className="w-16 h-16 shrink-0 rounded-sm bg-card overflow-hidden">
|
|
82
|
-
{image && <img src={image} alt={item.
|
|
89
|
+
{image && <img src={image} alt={item.name?.original || ""} className="w-full h-full object-cover" />}
|
|
83
90
|
</div>
|
|
84
91
|
<div className="flex-1 flex flex-col gap-1 min-w-0">
|
|
85
|
-
<span className="font-semibold truncate">{item.
|
|
86
|
-
{
|
|
92
|
+
<span className="font-semibold truncate">{item.name?.original}</span>
|
|
93
|
+
{/* V2 nests these under `attributes` — a top-level lineItem.descriptionLines no longer exists */}
|
|
94
|
+
{item.attributes?.descriptionLines?.map((dl, i) => (
|
|
87
95
|
<small key={i} className="text-muted-foreground">
|
|
88
96
|
{dl.name?.original}: {dl.plainText?.original || dl.colorInfo?.original}
|
|
89
97
|
</small>
|
|
90
98
|
))}
|
|
91
99
|
{isOut ? (
|
|
92
100
|
<small className="text-destructive">
|
|
93
|
-
{item.
|
|
101
|
+
{item.status === "PARTIALLY_IN_STOCK" ? "Not enough stock" : "No longer available"}
|
|
94
102
|
</small>
|
|
95
103
|
) : atStockLimit ? (
|
|
96
104
|
<small className="text-muted-foreground">Only {stock} left</small>
|
|
97
105
|
) : null}
|
|
98
106
|
<div className="flex items-center gap-2 mt-1">
|
|
99
|
-
<QtyButton disabled={loading ||
|
|
100
|
-
onClick={() => updateQuantity(item.id,
|
|
101
|
-
<span className="min-w-5 text-center" aria-label={`Quantity ${
|
|
107
|
+
<QtyButton disabled={loading || qty <= 1} label="Decrease quantity"
|
|
108
|
+
onClick={() => updateQuantity(item.id, qty - 1)}>−</QtyButton>
|
|
109
|
+
<span className="min-w-5 text-center" aria-label={`Quantity ${qty}`}>{qty}</span>
|
|
102
110
|
<QtyButton disabled={loading || atStockLimit} label="Increase quantity"
|
|
103
|
-
onClick={() => updateQuantity(item.id,
|
|
111
|
+
onClick={() => updateQuantity(item.id, qty + 1)}>+</QtyButton>
|
|
104
112
|
<button onClick={() => removeItem(item.id)} disabled={loading}
|
|
105
113
|
className="ml-auto border-none bg-transparent cursor-pointer text-muted-foreground text-[13px] underline disabled:opacity-50">Remove</button>
|
|
106
114
|
</div>
|
|
107
115
|
</div>
|
|
108
116
|
<div className="flex flex-col items-end shrink-0">
|
|
109
117
|
<span className="font-semibold">{paid}</span>
|
|
110
|
-
{struck && struck !== paid && (
|
|
111
|
-
<span className="text-muted-foreground line-through text-[13px]">{struck}</span>
|
|
112
|
-
)}
|
|
113
118
|
</div>
|
|
114
119
|
</div>
|
|
115
120
|
);
|
|
@@ -118,12 +123,6 @@ export default function CartDrawer() {
|
|
|
118
123
|
|
|
119
124
|
{lineItems.length > 0 && (
|
|
120
125
|
<footer className="p-4 border-t border-border flex flex-col gap-3">
|
|
121
|
-
{hasDiscount && (
|
|
122
|
-
<div className="flex justify-between items-baseline text-sm">
|
|
123
|
-
<span className="text-muted-foreground">Discount</span>
|
|
124
|
-
<span>−{discount}</span>
|
|
125
|
-
</div>
|
|
126
|
-
)}
|
|
127
126
|
{subtotal && (
|
|
128
127
|
<div className="flex justify-between items-baseline">
|
|
129
128
|
<span className="text-muted-foreground">Subtotal</span>
|
|
@@ -16,7 +16,7 @@ export function CartProvider({ children }) {
|
|
|
16
16
|
const [isOpen, setIsOpen] = useState(false);
|
|
17
17
|
const [loading, setLoading] = useState(false);
|
|
18
18
|
// The cart helpers throw on refusal with a buyer-readable reason — an empty cart, or line items
|
|
19
|
-
// whose
|
|
19
|
+
// whose status isn't IN_STOCK. Hold the message so the drawer can show it; without
|
|
20
20
|
// this the rejection is unhandled and the shopper sees the spinner stop with nothing said.
|
|
21
21
|
const [error, setError] = useState(null);
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ export function CartProvider({ children }) {
|
|
|
28
28
|
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
29
29
|
}, [refreshCart]);
|
|
30
30
|
|
|
31
|
-
const itemCount = (cart?.lineItems ?? []).reduce((n, li) => n + (li.
|
|
31
|
+
const itemCount = (cart?.lineItems ?? []).reduce((n, li) => n + (li.quantityInfo?.confirmedQuantity || 0), 0);
|
|
32
32
|
|
|
33
33
|
// Every mutation runs through here so a rejection always lands somewhere visible. `run` keeps the
|
|
34
34
|
// cart untouched on failure — the server stays the source of truth — and re-reads it for anything
|
|
@@ -7,20 +7,22 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
7
7
|
const STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Wix eCom Cart — key fields for building a cart UI.
|
|
10
|
+
* Wix eCom Cart V2 — key fields for building a cart UI.
|
|
11
11
|
* Full model: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/get-current-cart.md
|
|
12
12
|
*
|
|
13
|
-
* id {string}
|
|
13
|
+
* id {string} — the cart id IS the checkout id (currency lives under businessInfo.currencyCode / customerInfo.currencyCode, not at the cart root),
|
|
14
14
|
* lineItems[].id {string} — lineItemId for update/remove (NOT catalogItemId),
|
|
15
|
-
* lineItems[].
|
|
16
|
-
* lineItems[].catalogReference.catalogItemId {string},
|
|
17
|
-
* lineItems[].
|
|
18
|
-
* lineItems[].
|
|
19
|
-
* lineItems[].
|
|
20
|
-
*
|
|
15
|
+
* lineItems[].quantityInfo.confirmedQuantity {number},
|
|
16
|
+
* lineItems[].source.catalogReference.catalogItemId {string} — NOT top-level in V2,
|
|
17
|
+
* lineItems[].name.original {string},
|
|
18
|
+
* lineItems[].pricing.unitPrice {ConvertedMoney} — per-unit price after discounts,
|
|
19
|
+
* lineItems[].pricing.totalPrice {ConvertedMoney} — line total after discounts,
|
|
20
|
+
* ConvertedMoney is { amount, convertedAmount } with NO formatted string — `amount` is in the
|
|
21
|
+
* site currency, `convertedAmount` in the display currency; format the number client-side.
|
|
22
|
+
* lineItems[].attributes.descriptionLines {array} — human-readable option/modifier labels:
|
|
21
23
|
* [{ name: { original }, plainText: { original } OR colorInfo: { original, code } }],
|
|
22
|
-
* lineItems[].image
|
|
23
|
-
* lineItems[].
|
|
24
|
+
* lineItems[].attributes.image {object} — { id, url, height, width, altText } (use .url),
|
|
25
|
+
* lineItems[].status {string} — "IN_STOCK"|"PARTIALLY_IN_STOCK"|"OUT_OF_STOCK"|"REMOVED_FROM_CATALOG"
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -52,20 +54,22 @@ export async function addToCart(catalogItemId, variantId, quantity = 1, { modifi
|
|
|
52
54
|
|
|
53
55
|
const catalogReference = { appId: STORES_APP_ID, catalogItemId };
|
|
54
56
|
if (Object.keys(catalogReferenceOptions).length) catalogReference.options = catalogReferenceOptions;
|
|
55
|
-
const res = await wixApiRequest("/ecom/
|
|
57
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/add-line-items", {
|
|
56
58
|
method: "POST",
|
|
57
|
-
body: {
|
|
59
|
+
body: { catalogItems: [{ catalogReference, quantity }] },
|
|
58
60
|
});
|
|
61
|
+
// V2 nests the reference under `source` — a top-level lineItem.catalogReference no longer exists.
|
|
59
62
|
const line = (res?.cart?.lineItems ?? []).find(
|
|
60
|
-
(l) => l.catalogReference?.catalogItemId === catalogItemId && (!variantId || l.catalogReference?.options?.variantId === variantId),
|
|
63
|
+
(l) => l.source?.catalogReference?.catalogItemId === catalogItemId && (!variantId || l.source?.catalogReference?.options?.variantId === variantId),
|
|
61
64
|
);
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Cart V2 rejects an unbuyable line with an explicit error rather than silently dropping it, but
|
|
66
|
+
// guard both signals defensively:
|
|
67
|
+
// 1. status set to something other than IN_STOCK
|
|
68
|
+
// 2. no matching line at all (confirmed quantity 0 / line absent)
|
|
69
|
+
if (line?.status && line.status !== "IN_STOCK") {
|
|
70
|
+
throw new Error(`Item not available for sale (status: ${line.status}). Is it in stock?`);
|
|
67
71
|
}
|
|
68
|
-
if (!line || line.
|
|
72
|
+
if (!line || line.quantityInfo?.confirmedQuantity === 0) {
|
|
69
73
|
// A missing line usually means a required selection wasn't sent — a mandatory modifier
|
|
70
74
|
// (pass modifierChoices/customTextFields) or the variantId for a product with options —
|
|
71
75
|
// not necessarily out of stock. Verify every required choice is included in this call.
|
|
@@ -81,7 +85,7 @@ export async function addToCart(catalogItemId, variantId, quantity = 1, { modifi
|
|
|
81
85
|
/** Read the visitor's current cart. Returns null if no cart exists yet. */
|
|
82
86
|
export async function getCurrentCart() {
|
|
83
87
|
try {
|
|
84
|
-
const res = await wixApiRequest("/ecom/
|
|
88
|
+
const res = await wixApiRequest("/ecom/v2/carts/current", { method: "GET" });
|
|
85
89
|
return res?.cart ?? null;
|
|
86
90
|
} catch {
|
|
87
91
|
return null;
|
|
@@ -89,7 +93,9 @@ export async function getCurrentCart() {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|
|
92
|
-
*
|
|
96
|
+
* Start the hosted checkout for the current cart and return its URL.
|
|
97
|
+
* In Cart V2 the cart id IS the checkout id — there is no separate checkout-creation call; the
|
|
98
|
+
* redirect session is created straight from the current cart's id.
|
|
93
99
|
* Throws on empty cart, unavailable lines, or a missing redirect URL.
|
|
94
100
|
* Usage: window.location.href = await checkout()
|
|
95
101
|
* @returns {Promise<string>}
|
|
@@ -98,22 +104,16 @@ export async function checkout() {
|
|
|
98
104
|
const cart = await getCurrentCart();
|
|
99
105
|
const lines = cart?.lineItems ?? [];
|
|
100
106
|
if (!lines.length) throw new Error("Cannot check out: the cart is empty.");
|
|
101
|
-
const unavailable = lines.filter((l) => l.
|
|
107
|
+
const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
|
|
102
108
|
if (unavailable.length) {
|
|
103
|
-
const names = unavailable.map((l) => l.
|
|
109
|
+
const names = unavailable.map((l) => l.name?.original ?? l.source?.catalogReference?.catalogItemId).join(", ");
|
|
104
110
|
throw new Error(`Cannot check out: ${unavailable.length} item(s) not available — ${names}.`);
|
|
105
111
|
}
|
|
106
|
-
|
|
107
|
-
const checkoutRes = await wixApiRequest("/ecom/v1/carts/current/create-checkout", {
|
|
108
|
-
method: "POST",
|
|
109
|
-
body: { channelType: "WEB" },
|
|
110
|
-
});
|
|
111
|
-
const checkoutId = checkoutRes?.checkoutId;
|
|
112
|
-
if (!checkoutId) throw new Error("Failed to create checkout from the current cart.");
|
|
112
|
+
if (!cart.id) throw new Error("Failed to check out: the current cart has no id.");
|
|
113
113
|
|
|
114
114
|
const redirect = await wixApiRequest("/headless/v1/redirect-session", {
|
|
115
115
|
method: "POST",
|
|
116
|
-
body: { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl: window.location.href } },
|
|
116
|
+
body: { ecomCheckout: { checkoutId: cart.id }, callbacks: { postFlowUrl: window.location.href } },
|
|
117
117
|
});
|
|
118
118
|
const url = redirect?.redirectSession?.fullUrl;
|
|
119
119
|
if (!url) throw new Error("Failed to create the checkout redirect session.");
|
|
@@ -122,13 +122,13 @@ export async function checkout() {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Update the quantity of a cart line. lineItemId is cart.lineItems[].id, not catalogItemId.
|
|
125
|
-
*
|
|
125
|
+
* Cart V2 rejects an over-stock quantity with an explicit error rather than clamping it.
|
|
126
126
|
* @returns {Promise<object>} Updated cart.
|
|
127
127
|
*/
|
|
128
128
|
export async function updateCartItemQuantity(lineItemId, quantity) {
|
|
129
|
-
const res = await wixApiRequest("/ecom/
|
|
129
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/update-line-items", {
|
|
130
130
|
method: "POST",
|
|
131
|
-
body: { lineItems: [{
|
|
131
|
+
body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] },
|
|
132
132
|
});
|
|
133
133
|
return res?.cart;
|
|
134
134
|
}
|
|
@@ -139,7 +139,7 @@ export async function updateCartItemQuantity(lineItemId, quantity) {
|
|
|
139
139
|
* @returns {Promise<object>} Updated cart.
|
|
140
140
|
*/
|
|
141
141
|
export async function removeFromCart(lineItemId) {
|
|
142
|
-
const res = await wixApiRequest("/ecom/
|
|
142
|
+
const res = await wixApiRequest("/ecom/v2/carts/current/remove-line-items", {
|
|
143
143
|
method: "POST",
|
|
144
144
|
body: { lineItemIds: [lineItemId] },
|
|
145
145
|
});
|