@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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +134 -120
  3. package/skills/wix-app/references/EMBEDDED_SCRIPT.md +6 -0
  4. package/skills/wix-app/references/SITE_PLUGIN.md +2 -2
  5. package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +2 -2
  6. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +182 -99
  7. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +92 -47
  8. package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +15 -6
  9. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +275 -0
  10. package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +29 -42
  11. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +153 -449
  12. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +66 -19
  13. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +53 -158
  14. package/skills/wix-app/references/editor-react-component/EDITOR-EXTENSION-CONFIGURATION.md +78 -0
  15. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +42 -134
  16. package/skills/wix-app/references/editor-react-component/MANIFEST-ERRORS.md +27 -0
  17. package/skills/wix-app/references/editor-react-component/PARTS.md +45 -24
  18. package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +63 -179
  19. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +79 -163
  20. package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +36 -21
  21. package/skills/wix-app/references/editor-react-component/SSR.md +30 -99
  22. package/skills/wix-base44-connector/SKILL.md +34 -17
  23. package/skills/wix-base44-connector/scripts/utils.js +33 -25
  24. package/skills/wix-headless/entry/bootstrap.mjs +9 -2
  25. package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
  26. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +42 -23
  27. package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +10 -8
  28. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +27 -24
  29. package/skills/wix-headless/references/managed/AUTHENTICATION.md +5 -0
  30. package/skills/wix-manage/SKILL.md +14 -2
  31. package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +20 -13
  32. package/skills/wix-manage/references/bookings/create-class-service.md +1 -1
  33. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +24 -11
  34. package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +5 -5
  35. package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +182 -5
  36. package/skills/wix-manage/references/seo/manage-seo-tags.md +315 -0
  37. package/skills/wix-manage/references/seo/manage-url-redirects.md +250 -0
  38. package/skills/wix-manage/references/sites/query-sites.md +72 -161
  39. package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +5 -6
  40. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -9
  41. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +10 -7
  42. package/skills/wix-vibe-headless/platforms/base44.md +13 -10
  43. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +2 -0
  44. package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-checkout.js +9 -8
  45. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +4 -1
  46. package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartDrawer.jsx +23 -11
  47. package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +1 -1
  48. package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +29 -31
  49. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +15 -8
  50. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +37 -38
  51. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +2 -2
  52. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +35 -35
  53. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW-REFERENCE.md +0 -172
  54. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +0 -143
  55. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +0 -484
  56. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +0 -225
  57. package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +0 -6
  58. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +0 -95
@@ -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}, currency {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[].quantity {number},
18
- * lineItems[].catalogReference.catalogItemId {string} — the ordered item's GUID,
19
- * lineItems[].productName.original {string},
20
- * lineItems[].price.formattedAmount {string},
21
- * lineItems[].availability.status {string} — "AVAILABLE"|"NOT_AVAILABLE"|"PARTIALLY_AVAILABLE"|"NOT_FOUND"
22
- * Full model: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/cart/cart-object.md
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/v1/carts/current/add-to-cart
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/v1/carts/current/add-to-cart", {
78
+ const res = await wixApiRequest("/ecom/v2/carts/current/add-line-items", {
78
79
  method: "POST",
79
80
  body: {
80
- lineItems: [{ catalogReference: { appId: RESTAURANTS_ORDERS_APP_ID, catalogItemId: itemId, options }, quantity }],
81
+ catalogItems: [{ catalogReference: { appId: RESTAURANTS_ORDERS_APP_ID, catalogItemId: itemId, options }, quantity }],
81
82
  },
82
83
  });
83
- const line = (res?.cart?.lineItems ?? []).find((l) => l.catalogReference?.catalogItemId === itemId);
84
- if (line?.availability?.status && line.availability.status !== "AVAILABLE") {
85
- throw new Error(`Item not available to order (status: ${line.availability.status}).`);
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/v1/carts/current", { method: "GET" });
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/v1/carts/current/update-line-items-quantity
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/v1/carts/current/update-line-items-quantity", {
108
+ const res = await wixApiRequest("/ecom/v2/carts/current/update-line-items", {
107
109
  method: "POST",
108
- body: { lineItems: [{ id: lineItemId, quantity }] },
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/v1/carts/current/remove-line-items
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/v1/carts/current/remove-line-items", {
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
- * Create a checkout from the current cart and return the Wix-hosted checkout URL.
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.availability?.status && l.availability.status !== "AVAILABLE");
140
+ const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
137
141
  if (unavailable.length) {
138
- const names = unavailable.map((l) => l.productName?.original ?? l.catalogReference?.catalogItemId).join(", ");
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 that stopped being AVAILABLE — reaches the buyer instead
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, formattedAmount, formattedConvertedAmount } —
253
- // show a formatted one so the currency symbol and grouping come from Wix. Read
254
- // `cart.subtotalAfterDiscounts` rather than `cart.subtotal`: the two match until a cart-level coupon
255
- // applies, and then `subtotal` is the pre-discount figure. `cart.discount` + `cart.appliedDiscounts`
256
- // carry the reduction. Never sum line items yourself — tax and shipping resolve at checkout.
257
- // `lineItems[].availability.quantityAvailable` is the stock cap for a quantity control.
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 — `price` is post-discount, `fullPrice` is before it, so the
6
- // two together are the strikethrough. Never compute a total in the client: tax, shipping and
7
- // promotions are resolved server-side, so `subtotal` is the only figure safe to show pre-checkout.
8
- // Money objects carry { amount, convertedAmount, formattedAmount, formattedConvertedAmount }; use a
9
- // formatted one so the currency symbol and grouping come from Wix. Prefer `subtotalAfterDiscounts`
10
- // over `subtotal` — the two are equal until a cart-level coupon applies, and then `subtotal` is the
11
- // pre-discount figure, which would quote the buyer more than they'll pay.
12
- // • `availability.quantityAvailable` caps the stepper, so exceeding stock is refused here rather than
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
- // • `availability.status` is flagged per line here because checkout() refuses the whole cart when
15
- // any item isn't AVAILABLE — showing it on the row is what makes that refusal understandable.
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
- const money = (m) => m?.formattedConvertedAmount || m?.formattedAmount;
24
- const subtotal = money(cart?.subtotalAfterDiscounts) || money(cart?.subtotal);
25
- const discount = money(cart?.discount);
26
- const hasDiscount = (cart?.appliedDiscounts?.length || 0) > 0 && Number(cart?.discount?.amount) > 0;
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 struck = item.fullPrice?.formattedAmount;
75
- const paid = item.price?.formattedAmount;
76
- const isOut = item.availability?.status && item.availability.status !== "AVAILABLE";
77
- const stock = item.availability?.quantityAvailable;
78
- const atStockLimit = Number.isFinite(stock) && item.quantity >= 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.productName?.original || ""} className="w-full h-full object-cover" />}
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.productName?.original}</span>
86
- {item.descriptionLines?.map((dl, i) => (
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.availability.status === "NOT_AVAILABLE" ? "No longer available" : "Not enough stock"}
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 || item.quantity <= 1} label="Decrease quantity"
100
- onClick={() => updateQuantity(item.id, item.quantity - 1)}>−</QtyButton>
101
- <span className="min-w-5 text-center" aria-label={`Quantity ${item.quantity}`}>{item.quantity}</span>
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, item.quantity + 1)}>+</QtyButton>
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 availability.status isn't AVAILABLE. Hold the message so the drawer can show it; without
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.quantity || 0), 0);
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}, currency {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[].quantity {number},
16
- * lineItems[].catalogReference.catalogItemId {string},
17
- * lineItems[].productName.original {string},
18
- * lineItems[].price.formattedAmount {string} — after discounts with currency symbol,
19
- * lineItems[].fullPrice.formattedAmount {string} — before discount (strikethrough),
20
- * lineItems[].descriptionLines {array} — human-readable option/modifier labels:
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.url {string},
23
- * lineItems[].availability.status {string} — "AVAILABLE"|"NOT_AVAILABLE"|"PARTIALLY_AVAILABLE"|"NOT_FOUND"
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/v1/carts/current/add-to-cart", {
57
+ const res = await wixApiRequest("/ecom/v2/carts/current/add-line-items", {
56
58
  method: "POST",
57
- body: { lineItems: [{ catalogReference, quantity }] },
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
- // Wix returns 200 even when the line is silently rejected — guard both signals:
63
- // 1. availability.status set to something other than AVAILABLE
64
- // 2. no matching line at all (quantity 0 / line absent)
65
- if (line?.availability?.status && line.availability.status !== "AVAILABLE") {
66
- throw new Error(`Item not available for sale (status: ${line.availability.status}). Is it in stock?`);
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.quantity === 0) {
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/v1/carts/current", { method: "GET" });
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
- * Create a checkout from the current cart and return the hosted checkout URL.
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.availability?.status && l.availability.status !== "AVAILABLE");
107
+ const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
102
108
  if (unavailable.length) {
103
- const names = unavailable.map((l) => l.productName?.original ?? l.catalogReference?.catalogItemId).join(", ");
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
- * Wix caps the result at remaining stock — returned quantity may be lower than requested.
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/v1/carts/current/update-line-items-quantity", {
129
+ const res = await wixApiRequest("/ecom/v2/carts/current/update-line-items", {
130
130
  method: "POST",
131
- body: { lineItems: [{ id: lineItemId, quantity }] },
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/v1/carts/current/remove-line-items", {
142
+ const res = await wixApiRequest("/ecom/v2/carts/current/remove-line-items", {
143
143
  method: "POST",
144
144
  body: { lineItemIds: [lineItemId] },
145
145
  });