@wix/agent-skills 1.16.3 → 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 -127
- 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 -48
- 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 +53 -29
- 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 +11 -3
- package/skills/wix-app/references/editor-react-component/PARTS.md +46 -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 +11 -1
- package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +20 -13
- 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/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 -516
- 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
|
@@ -6,19 +6,22 @@ initial prompt, and the Wix connector is already configured; use it for all admi
|
|
|
6
6
|
Your Wix client id is in your prompt — a public, buyer-facing credential (anonymous visitor tokens
|
|
7
7
|
only), safe in the frontend; use it directly for the Wix client setup.
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> **The Wix skills installed below are the complete build and seed path for this app — the Wix connector supplies the token for admin API calls.** **Do NOT use the Base44 commerce kit (or any Base44 solution kit).**
|
|
10
10
|
|
|
11
11
|
Follow STEPs 1–5 below exactly (run STEP 4 in parallel with STEP 3).
|
|
12
12
|
|
|
13
13
|
## STEP 1 — Install the Wix skills locally
|
|
14
14
|
|
|
15
|
-
Install
|
|
15
|
+
Install three skills — they land under `.agents/skills/`:
|
|
16
16
|
- **`wix-vibe-headless`** — the client build **and** seed guide (STEPs 3–4): your main source of
|
|
17
17
|
truth. Seeding lives here too, per-vertical under `references/<vertical>/seed/`.
|
|
18
|
-
- **`wix-
|
|
19
|
-
|
|
18
|
+
- **`wix-manage`** — REST recipes for managing and configuring the site (install apps, seed
|
|
19
|
+
catalogs, business setup).
|
|
20
|
+
- **`wix-base44-connector`** — building on Wix from the Base44 sandbox: site context, API
|
|
21
|
+
contracts, and the docs **fallback** — find + read the Wix API docs for anything
|
|
22
|
+
`wix-vibe-headless` doesn't cover.
|
|
20
23
|
|
|
21
|
-
Run this through exec_tool, exactly as written — installs
|
|
24
|
+
Run this through exec_tool, exactly as written — installs all three skills, deploys REST scaffolds + UI into `src/`, writes `wix-config.js`, and pins the AGENTS.md note.
|
|
22
25
|
|
|
23
26
|
**Set `VERTICALS`** to what the prompt asks for (too vague to tell? do STEP 2 first, then set it):
|
|
24
27
|
|
|
@@ -46,7 +49,7 @@ const VERTICALS = ['storefront']; // ← set from the prompt; list every
|
|
|
46
49
|
const WIX_CLIENT_ID = '<client id from the prompt>';
|
|
47
50
|
const WIX_METASITE_ID = '<site id from the prompt>';
|
|
48
51
|
const results = {};
|
|
49
|
-
for (const skill of ['wix-vibe-headless', 'wix-
|
|
52
|
+
for (const skill of ['wix-vibe-headless', 'wix-manage', 'wix-base44-connector']) {
|
|
50
53
|
if (existsSync(`/app/.agents/skills/${skill}/SKILL.md`)) { results[skill] = 'already_installed'; continue; }
|
|
51
54
|
try {
|
|
52
55
|
const out = execSync(`CI=1 npx -y skills add wix/skills/skills/${skill} --yes 2>&1`,
|
|
@@ -103,7 +106,7 @@ cleanup truly seems needed, ask the user first.
|
|
|
103
106
|
Seed by calling your vertical's ready-made seed module — read
|
|
104
107
|
`.agents/skills/wix-vibe-headless/references/<vertical>/seed/SEED.md` and load its `seed-*.js` via
|
|
105
108
|
its loader snippet (build-time exec_tool); call its functions with your data. Gaps or an unexpected
|
|
106
|
-
shape → the **`wix-
|
|
109
|
+
shape → the **`wix-base44-connector`** skill's doc discovery.
|
|
107
110
|
|
|
108
111
|
**Auth for these admin calls is the already-configured Wix connector — nothing else.** Get its
|
|
109
112
|
access token and send it as a bearer token:
|
|
@@ -119,8 +122,8 @@ inline it throws *"Identifier 'base44' has already been declared."*).
|
|
|
119
122
|
|
|
120
123
|
**Entity images.** For image-bearing entities (store products, blog covers, bookings services,
|
|
121
124
|
restaurant items, portfolio projects, event heroes, CMS items), generate with **Base44's built-in
|
|
122
|
-
image generation**, then attach via the vertical seed module's image-attach step —
|
|
123
|
-
module doesn't cover that entity.
|
|
125
|
+
image generation**, then attach via the vertical seed module's image-attach step —
|
|
126
|
+
`wix-base44-connector` doc discovery if the module doesn't cover that entity.
|
|
124
127
|
|
|
125
128
|
**Seed images with the FINAL url, in one call.** Seeding writes to Wix, so use the real
|
|
126
129
|
`https://media.base44.com/...` url from the **completed** `generate_image` result and pass it straight
|
|
@@ -147,6 +150,6 @@ Once the site is built and seeded:
|
|
|
147
150
|
## Later admin requests
|
|
148
151
|
|
|
149
152
|
For any later admin/management request, work as in STEP 4: your vertical's seed module first, else
|
|
150
|
-
`wix-
|
|
153
|
+
`wix-base44-connector` doc discovery — all over the connector.
|
|
151
154
|
|
|
152
155
|
version: v1338
|
|
@@ -165,6 +165,8 @@ stale render.
|
|
|
165
165
|
## Using the client from your own UI
|
|
166
166
|
The two shipped hooks are the whole flow. If you build a custom surface, drive it the same way:
|
|
167
167
|
|
|
168
|
+
> 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.
|
|
169
|
+
|
|
168
170
|
```jsx
|
|
169
171
|
// LIST — every list helper returns an OBJECT, not a bare array. Destructure it:
|
|
170
172
|
const { services, total, nextOffset } = await queryServices({ limit: 24 }); // total 0 → empty state
|
|
@@ -3,7 +3,7 @@ import { wixApiRequest } from "./wix-client.js";
|
|
|
3
3
|
// Data model reference: see INSTRUCTIONS.md
|
|
4
4
|
// Service and TimeSlot shapes: see wix-bookings-services.js
|
|
5
5
|
|
|
6
|
-
// Wix Bookings app id — required inside catalogReference
|
|
6
|
+
// Wix Bookings app id — required inside catalogReference when adding the booking to the eCom cart.
|
|
7
7
|
const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -101,27 +101,28 @@ export async function createBooking(slot, contactDetails, { totalParticipants =
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Add a created booking to the current eCom cart and return the hosted checkout URL.
|
|
105
|
+
* In Cart V2 the cart id IS the checkout id — add the booking line to the current cart, then create
|
|
106
|
+
* the redirect session straight from that cart's id (there is no separate checkout-creation call).
|
|
105
107
|
* Redirect the buyer there (window.location.href = ...). On return, the booking is confirmed.
|
|
106
108
|
* Throws if no redirect URL is produced.
|
|
107
|
-
* https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/
|
|
109
|
+
* https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md
|
|
108
110
|
* @param {string} bookingId booking.id from createBooking().
|
|
109
111
|
* @returns {Promise<string>} The hosted-checkout URL to redirect to.
|
|
110
112
|
*/
|
|
111
113
|
export async function checkoutBooking(bookingId) {
|
|
112
114
|
if (!bookingId) throw new Error("checkoutBooking requires a bookingId.");
|
|
113
115
|
|
|
114
|
-
const
|
|
116
|
+
const cartRes = await wixApiRequest("/ecom/v2/carts/current/add-line-items", {
|
|
115
117
|
method: "POST",
|
|
116
118
|
body: {
|
|
117
|
-
|
|
118
|
-
lineItems: [
|
|
119
|
+
catalogItems: [
|
|
119
120
|
{ quantity: 1, catalogReference: { appId: BOOKINGS_APP_ID, catalogItemId: bookingId } },
|
|
120
121
|
],
|
|
121
122
|
},
|
|
122
123
|
});
|
|
123
|
-
const checkoutId =
|
|
124
|
-
if (!checkoutId) throw new Error("Failed to
|
|
124
|
+
const checkoutId = cartRes?.cart?.id;
|
|
125
|
+
if (!checkoutId) throw new Error("Failed to add the booking to the cart for checkout.");
|
|
125
126
|
|
|
126
127
|
const redirect = await wixApiRequest("/headless/v1/redirect-session", {
|
|
127
128
|
method: "POST",
|
|
@@ -170,6 +170,8 @@ render.
|
|
|
170
170
|
|
|
171
171
|
## Using the client from your own UI (menu, order cart)
|
|
172
172
|
|
|
173
|
+
> 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.
|
|
174
|
+
|
|
173
175
|
```jsx
|
|
174
176
|
import { getFullMenu } from "@/rest/wix-restaurants-menu";
|
|
175
177
|
import { useOrderCart } from "@/context/OrderCartContext";
|
|
@@ -188,7 +190,8 @@ const { menus } = await getFullMenu(); // [] when no menus → show t
|
|
|
188
190
|
// Load-bearing field paths (the shipped components already do these):
|
|
189
191
|
// - item.image / section.image / label.icon are OBJECTS → render `.url`, never the object; //-urls → https:
|
|
190
192
|
// - MENU prices are plain decimal strings with NO currency symbol ("12.50") — format in the UI.
|
|
191
|
-
// The eCom cart line price (line.
|
|
193
|
+
// The eCom cart line price (line.pricing.unitPrice / line.pricing.totalPrice) is a ConvertedMoney
|
|
194
|
+
// { amount, convertedAmount } with NO symbol either — format the number yourself.
|
|
192
195
|
// - an item is priced by EITHER item.price (single) OR item.variants[] (one-of, each { name, price }).
|
|
193
196
|
// - a cart mutation uses cart.lineItems[].id (the lineItemId), NOT the menu item id.
|
|
194
197
|
```
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// Slide-over order cart. Reads everything from useOrderCart(); mutate by lineItem.id (NOT the menu
|
|
2
|
-
// item id), check out via the context (redirect-session URL). The eCom
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// item id), check out via the context (redirect-session URL). The eCom Cart V2 line price is
|
|
3
|
+
// `pricing.totalPrice`, a ConvertedMoney { amount, convertedAmount } with NO formatted string —
|
|
4
|
+
// format it with the cart's currency (formatCartMoney below). Styled with base44 design tokens
|
|
5
|
+
// (shadcn Tailwind classes). Mount it once in the Layout.
|
|
5
6
|
import { useOrderCart } from "@/context/OrderCartContext";
|
|
6
7
|
|
|
8
|
+
// Cart V2 money is a ConvertedMoney { amount, convertedAmount } with NO formatted string — format it
|
|
9
|
+
// with the cart's currency (buyer's display currency when present, else the site currency).
|
|
10
|
+
function formatCartMoney(money, cart) {
|
|
11
|
+
const value = money?.convertedAmount ?? money?.amount;
|
|
12
|
+
const currency = cart?.customerInfo?.currencyCode ?? cart?.businessInfo?.currencyCode ?? "USD";
|
|
13
|
+
return value == null ? "" : new Intl.NumberFormat(undefined, { style: "currency", currency }).format(Number(value));
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export default function OrderCartDrawer() {
|
|
8
17
|
const { cart, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading } = useOrderCart();
|
|
9
18
|
const lineItems = cart?.lineItems ?? [];
|
|
@@ -22,23 +31,26 @@ export default function OrderCartDrawer() {
|
|
|
22
31
|
<div className="flex-1 overflow-y-auto p-4">
|
|
23
32
|
{lineItems.length === 0 ? (
|
|
24
33
|
<p className="text-muted-foreground">Your order is empty.</p>
|
|
25
|
-
) : lineItems.map((line) =>
|
|
34
|
+
) : lineItems.map((line) => {
|
|
35
|
+
const qty = line.quantityInfo?.confirmedQuantity;
|
|
36
|
+
return (
|
|
26
37
|
<div key={line.id} className="flex gap-3 py-3 border-b border-border">
|
|
27
38
|
{/* line.id is the lineItemId for mutations — NOT the menu item id */}
|
|
28
39
|
<div className="flex-1 flex flex-col gap-1">
|
|
29
|
-
<span className="font-semibold">{line.
|
|
40
|
+
<span className="font-semibold">{line.name?.original}</span>
|
|
30
41
|
<div className="flex items-center gap-2 mt-1">
|
|
31
|
-
<QtyButton onClick={() => updateQuantity(line.id, Math.max(1,
|
|
32
|
-
<span className="min-w-[20px] text-center">{
|
|
33
|
-
<QtyButton onClick={() => updateQuantity(line.id,
|
|
42
|
+
<QtyButton onClick={() => updateQuantity(line.id, Math.max(1, qty - 1))}>−</QtyButton>
|
|
43
|
+
<span className="min-w-[20px] text-center">{qty}</span>
|
|
44
|
+
<QtyButton onClick={() => updateQuantity(line.id, qty + 1)}>+</QtyButton>
|
|
34
45
|
<button onClick={() => removeItem(line.id)}
|
|
35
46
|
className="ml-auto border-none bg-transparent cursor-pointer text-muted-foreground text-[13px] underline">Remove</button>
|
|
36
47
|
</div>
|
|
37
48
|
</div>
|
|
38
|
-
{/*
|
|
39
|
-
<span className="font-semibold">{line.
|
|
49
|
+
{/* Cart V2 has no formatted string — format the ConvertedMoney client-side */}
|
|
50
|
+
<span className="font-semibold">{formatCartMoney(line.pricing?.totalPrice, cart)}</span>
|
|
40
51
|
</div>
|
|
41
|
-
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
42
54
|
</div>
|
|
43
55
|
|
|
44
56
|
{lineItems.length > 0 && (
|
|
@@ -30,7 +30,7 @@ export function OrderCartProvider({ children }) {
|
|
|
30
30
|
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
31
31
|
}, [refreshCart]);
|
|
32
32
|
|
|
33
|
-
const itemCount = (cart?.lineItems ?? []).reduce((n, li) => n + (li.
|
|
33
|
+
const itemCount = (cart?.lineItems ?? []).reduce((n, li) => n + (li.quantityInfo?.confirmedQuantity || 0), 0);
|
|
34
34
|
|
|
35
35
|
// A menu item carries its menuId/sectionId from the getFullMenu tree — the caller passes both.
|
|
36
36
|
// addItemToCart throws on a missing operation/menu/section or an unavailable line; let it propagate.
|
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
|