Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e70853e
docs(ecom): migrate storefront cart/checkout references to Cart V2
rommy-amitai-w Aug 5, 2026
d0a3d04
docs(ecom): fix residual currentCart -> currentCartV2 in store recipe
rommy-amitai-w Aug 6, 2026
c3647d1
docs(ecom): migrate remaining cart/checkout references to Cart V2
rommy-amitai-w Aug 6, 2026
ced9373
docs(ecom): fix missed currentCart import in restaurant-orders non-As…
rommy-amitai-w Aug 6, 2026
50a89fb
docs(ecom): fix membership shape + money-format notes from independen…
rommy-amitai-w Aug 6, 2026
9b19a01
docs(ecom): fix attributes.image doc shape (2nd review)
rommy-amitai-w Aug 6, 2026
a34b1bf
docs(ecom): 3rd-review minors — currentCart prose + membership caveat
rommy-amitai-w Aug 6, 2026
00961ec
docs(ecom): 4th-review nit — match sibling doc-link style (drop .md)
rommy-amitai-w Aug 6, 2026
70c32e3
docs(ecom): membership uses public selectedMembership (compiler-verif…
rommy-amitai-w Aug 6, 2026
f940698
docs(replatform): fully align eCommerce taxonomy to Cart V2
rommy-amitai-w Aug 6, 2026
33fb525
docs(ecom): add price-formatting guidance for Cart V2 (no preformatte…
rommy-amitai-w Aug 9, 2026
56772ee
docs(ecom): fix calculateCart -> calculateCurrentCart in store recipe
rommy-amitai-w Aug 9, 2026
6d70f41
docs(ecom): name the entity "cart", not "checkout" (V2 unification)
rommy-amitai-w Aug 9, 2026
aa06d5e
docs(ecom): tidy "Cart V2" -> "cart" where already established
rommy-amitai-w Aug 9, 2026
8494521
docs(replatform): correct — calculated totals are NOT stored on the cart
rommy-amitai-w Aug 9, 2026
7c40d85
docs(replatform): keep the 'checkout state' trigger terse
rommy-amitai-w Aug 9, 2026
9522564
docs(ecom): de-clutter per-line V1 asides; add one migration-guide link
rommy-amitai-w Aug 9, 2026
9d23342
docs(ecom): drop remaining createCheckoutFromCurrentCart aside in res…
rommy-amitai-w Aug 9, 2026
922f561
docs(ecom): add migration-guide link to remaining 'One entity now' notes
rommy-amitai-w Aug 9, 2026
12b36a9
docs(ecom): disambiguate the Checkout step (heading + cross-ref)
rommy-amitai-w Aug 9, 2026
74583ed
docs(ecom): payment figures come from CartSummary; drop stray asides
rommy-amitai-w Aug 10, 2026
1a50d5a
docs(ecom): finish Cart V2-only pass + explicit-error semantics + mem…
rommy-amitai-w Aug 13, 2026
9b0ce3c
docs(ecom): address independent-review findings on pricing-plans + vi…
rommy-amitai-w Aug 13, 2026
b47bf75
docs(ecom): de-duplicate pricing-plans membership-eligibility guidance
rommy-amitai-w Aug 16, 2026
99b883b
docs(ecom): lead credit/session eligibility with the CartSummary/calc…
rommy-amitai-w Aug 16, 2026
01c20d4
docs(ecom): credit balance is a Benefit-Program domain read, not an e…
rommy-amitai-w Aug 16, 2026
db163bf
Merge remote-tracking branch 'origin/main' into cart-v2-migration
rommy-amitai-w Aug 17, 2026
4dbc608
docs(ecom): re-apply Cart V2 migration to main's rebuilt vibe-headless
rommy-amitai-w Aug 17, 2026
491a79a
docs(ecom): migrate vibe-headless cart UI consumers to Cart V2 fields
rommy-amitai-w Aug 17, 2026
f39a995
test(evalforge): add coverage scenarios for the V2-migrated wix-manag…
rommy-amitai-w Aug 17, 2026
854b6d6
revert(wix-app): drop the backend-event + site-plugin cart-v2 edits
rommy-amitai-w Aug 17, 2026
6712dc1
Merge remote-tracking branch 'origin/main' into cart-v2-migration
rommy-amitai-w Aug 18, 2026
325f7f9
fix(vibe-headless): read V2 line-item paths — source.catalogReference…
Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/wix-headless/references/SDK_HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Everything else the host resolves live from the queries in §3. (Static platform
- Frontend CMS access is **read**; visitor writes go through **Forms** submissions.
- **Blog comments are member-gated — read public, write authenticated.** Querying/rendering comments is public, so list them server-side in SSR. **Submitting** needs a logged-in member, so don't gate the form behind an upfront login check (that branch is what burns deliberation): render the form always and resolve identity at submit — POST to a backend endpoint (`src/pages/api/*.ts`) that calls `createComment` with the request session; if the caller isn't a member, redirect to the built-in `/api/auth/login?returnUrl=…`. **Do not build the comment form as a client island** — the API-endpoint + session path is the documented shape and avoids the browser-auth detour. The comment API keys (post `referenceId`, Blog appDefId, author lookup via `post.memberId`) are in `how-to-code-a-blog.md`.
- **Member auth is one mechanism split on the frontend axis (the §3 members note routes to the right recipe), and orthogonal to elevation.** Sign-up and log-in are the *same* flow (the Wix login page logs in **or** registers); log-out is its inverse. Two layers stay separate: **identity** (logged-in vs not — no app install) vs **profile** (name/photo/roles — needs the **Wix Members Area app** installed, `SETUP.md`). **pricing-plans is a hard dependency**: subscribing requires a logged-in member; for the other verticals, member login is a soft add-on for their "my …" surfaces only. **A member reading their own data (own orders/bookings/subscriptions, plan-gated content) uses the member token with NO `auth.elevate`** — elevation is the separate admin/permission axis (site-wide reads, server-side only), not something member features need.
- **Gating is a LIVE signal, never a hardcoded id list.** Whenever content is gated (members-only articles, plan-eligible class booking, any "premium" surface), decide eligibility from a signal read **at request time** — a queryable flag carried on the content (a `members-only` blog category/tag or a boolean CMS field) and live coverage/eligibility (`checkout.membershipOptions.eligibleMemberships`, or the member's active-order `planId`s matched against **live** Benefit-Program coverage). **Never** gate on a frozen set of "premium" slugs/ids or a seed-time `plan→service` map: a gated post or a newly-covered service the owner adds later would silently read as public/ineligible. See `how-to-code-a-blog.md` (member features) and `how-to-code-pricing-plans.md` (coverage read).
- **Gating is a LIVE signal, never a hardcoded id list.** Whenever content is gated (members-only articles, plan-eligible class booking, any "premium" surface), decide eligibility from a signal read **at request time** — a queryable flag carried on the content (a `members-only` blog category/tag or a boolean CMS field) and live coverage/eligibility (Cart V2's `currentCartV2.calculateCurrentCart()` → `summary.paymentSummary.memberships`, or the member's active-order `planId`s matched against **live** Benefit-Program coverage). **Never** gate on a frozen set of "premium" slugs/ids or a seed-time `plan→service` map: a gated post or a newly-covered service the owner adds later would silently read as public/ineligible. See `how-to-code-a-blog.md` (member features) and `how-to-code-pricing-plans.md` (coverage read).

### 6 · What a complete site must include (per loaded capability)
For each loaded capability, carry its **Required site features** and **Implementation checklist** from `references/CAPABILITIES.md` into the guide — in plain product language, lightly tailored to what was seeded. **This is the build spec, not optional polish:** the host should build every *required feature* and cover every *checklist* item. For example, a blog must show the **author** (name + photo), the publish date and reading time, the cover image, and the **full formatted content** (not flattened text) — a posts-list-plus-plain-text-body is incomplete. The host maps these onto its own components using the packages/docs in §3 and the seeded schema in §4.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ A concise contract for writing the **frontend code** of a storefront against a C
| Products (list, get, search, filter) | `@wix/stores` | `productsV3` |
| Variants (to resolve `variantId`) | `@wix/stores` | `readOnlyVariantsV3` |
| Categories | `@wix/stores` | `categories` |
| Cart (add / get / checkout) | `@wix/ecom` | `currentCart` |
| Cart (add / get / checkout) | `@wix/ecom` | `currentCartV2` |
| Redirect to hosted checkout | `@wix/redirects` | `redirects` |

> Migrating from Cart V1 / Checkout V1? The code below is 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.

**Never** import the V1 `products` or `collections` modules from `@wix/stores`.

**Auth / client — framework split:**
- **Astro (Wix-managed):** authentication is ambient. Call `currentCart` / `productsV3` / `readOnlyVariantsV3` directly from server components and backend routes (`src/pages/api/*.ts`) — **no `createClient`, no `OAuthStrategy`, no `clientId`.**
- **Astro (Wix-managed):** authentication is ambient. Call `currentCartV2` / `productsV3` / `readOnlyVariantsV3` directly from server components and backend routes (`src/pages/api/*.ts`) — **no `createClient`, no `OAuthStrategy`, no `clientId`.**
- **Non-Astro (Vite/React/Vue/static):** build one manual visitor client and reuse it:
```js
import { createClient, OAuthStrategy } from '@wix/sdk';
import { productsV3, readOnlyVariantsV3 } from '@wix/stores';
import { currentCart } from '@wix/ecom';
import { currentCartV2 } from '@wix/ecom';
import { redirects } from '@wix/redirects';

const client = createClient({
modules: { productsV3, readOnlyVariantsV3, currentCart, redirects },
modules: { productsV3, readOnlyVariantsV3, currentCartV2, redirects },
auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
});
```
Expand All @@ -49,7 +51,7 @@ A concise contract for writing the **frontend code** of a storefront against a C

## The shapes you read (field cheat-sheet)

The exact field paths the storefront reads, and the **plausible-wrong sibling** each is mistaken for — the sections below reference these instead of re-describing them. All `amount`s are **strings**. These are **read** shapes; the cart-add body (under *Adding to cart*) is a separate **write** shape, and the `_id` rule applies to read **entities**, not to method-return wrappers (note `checkoutId`).
The exact field paths the storefront reads, and the **plausible-wrong sibling** each is mistaken for — the sections below reference these instead of re-describing them. All `amount`s are **strings**. These are **read** shapes; the cart-add body (under *Adding to cart*) is a separate **write** shape, and the `_id` rule applies to read **entities**, not to request params (note the redirect session's `checkoutId`, which is just the cart's `_id`).

```jsonc
// productsV3.queryProducts().…find() → result.items[]
Expand All @@ -72,11 +74,12 @@ variant = {
inventoryStatus: { inStock }, // variant-level stock (boolean)
}

// currentCart.getCurrentCart() → { lineItems: [...] }
lineItem = { quantity, price: { amount }, image } // price is HERE (NOT actualPriceRange); image is wix:image:// too → resolve
// currentCartV2.getCurrentCart() → { cart: { _id, lineItems: [...] } } // NOTE: returns { cart } — destructure it
lineItem = { _id, name: { original }, quantityInfo: { confirmedQuantity }, pricing: { unitPrice: { amount } }, attributes: { image } }
// price → pricing.unitPrice (ConvertedMoney, NO formatted string in V2 — format it yourself; .amount is site currency, .convertedAmount the buyer's display currency); qty → quantityInfo.confirmedQuantity; image → attributes.image (wix:image:// → resolve)

// currentCart.createCheckoutFromCurrentCart({ channelType }) → { checkoutId } // a STRING — NOT { checkout }, NOT _id
// redirects.createRedirectSession({ ecomCheckout: { checkoutId }, callbacks }) → { redirectSession: { fullUrl } }
// the cart's _id is the checkout id → pass to the redirect session:
// redirects.createRedirectSession({ ecomCheckout: { checkoutId: cart._id }, callbacks }) → { redirectSession: { fullUrl } }
```

---
Expand All @@ -94,7 +97,7 @@ Doc: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v

**⚠️ CRITICAL: the entity id is `_id`, NOT `id`.** The SDK normalizes every entity's id to **`_id`**. `product.id` is `undefined` in SDK code. This is the cart-killer: feeding `product.id` into the cart's `catalogItemId` sends an empty string and the add returns **HTTP 500** (`"catalogItemId" has size 0`). Use `product._id` everywhere — in links, as the cart `catalogItemId`, and as the variant-query filter value. (If a field name surprises you, you are probably reading the REST doc view — re-open it with `?apiView=SDK`.)

**Scope of the `_id` rule — entity reads only.** `_id` is the id of a read **entity** (product, variant, cart line item). It is **not** a universal "every id field is `_id`" rule: method results name their own fields (e.g. `createCheckoutFromCurrentCart` returns `checkoutId`, *not* `_id` — see Checkout). Don't assume a method's return wrapper exposes `_id`.
**Scope of the `_id` rule — entity reads only.** `_id` is the id of a read **entity** (product, variant, cart line item). It is **not** a universal "every id field is `_id`" rule: request params name their own fields (e.g. the redirect session takes `ecomCheckout.checkoutId`, *not* `_id` — see the *Checkout* section below — even though the value you pass is the cart's `_id`). Don't assume every id-shaped field is spelled `_id`.

**Visibility:** only `visible: true` products are returned to a visitor token, so a missing product usually means it wasn't seeded visible — not a query bug.

Expand Down Expand Up @@ -149,11 +152,11 @@ Doc: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v

Each `variant` carries `variant.optionChoices[].optionChoiceNames` — `{ optionName, choiceName }`. Match the buyer's selected options (Size = "Small", Color = "Red", …) against those names to pick the variant. For a **single-variant** product, use the only item. Fall back to `items[0]` if matching yields nothing. The id to send to the cart is **`variant.variantId ?? variant._id`**.

**2 · Add it.** Doc: <https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/add-to-current-cart.md?apiView=SDK> · catalogReference contract: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/e-commerce-integration.md?apiView=SDK>
**2 · Add it.** Doc: <https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md?apiView=SDK> · catalogReference contract: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/e-commerce-integration.md?apiView=SDK>

```js
await currentCart.addToCurrentCart({
lineItems: [{
await currentCartV2.addLineItemsToCurrentCart({
catalogItems: [{ // the write shape uses `catalogItems`
quantity,
catalogReference: {
catalogItemId: product._id, // the product's _id (the `_id` rule above)
Expand All @@ -164,28 +167,44 @@ await currentCart.addToCurrentCart({
});
```

**⚠️ CRITICAL: `options.variantId` is MANDATORY for any product that has variants.** Adding by `catalogItemId` alone returns **HTTP 200 but adds nothing** — the silent empty cart. The cart method's required-params list omits `variantId`, so this fails quietly and looks like success. Always resolve and include it (part 1 above).
**⚠️ CRITICAL: `options.variantId` is MANDATORY for any product that has variants.** Adding by `catalogItemId` alone **fails** — the catalog can't resolve a variant-bearing product without it, and Cart V2 **rejects the add with an explicit error** rather than accepting an invalid line. The cart method's required-params list omits `variantId`, so it's an easy one to miss. Always resolve and include it (part 1 above).

**⚠️ CRITICAL: `options.options` is for MODIFIERS, not variant selection.** Product option selections (Size/Color) are resolved to a **variant** and referenced by `variantId`. `options.options` is only for free-text / TEXT_CHOICES add-on **modifiers**. Do **not** encode Size/Color as `options.options` — that is the coffee-grind bug (`200` + empty cart).
**⚠️ CRITICAL: `options.options` is for MODIFIERS, not variant selection.** Product option selections (Size/Color) are resolved to a **variant** and referenced by `variantId`. `options.options` is only for free-text / TEXT_CHOICES add-on **modifiers**. Do **not** encode Size/Color as `options.options` — that is the coffee-grind bug: the variant never resolves, so Cart V2 rejects the add with an explicit error.

### Checkout
### Checkout — redirect to the hosted checkout page

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part especially needs verification


Create a checkout from the current cart, then redirect the buyer to the hosted checkout.
Docs: <https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/create-checkout-from-current-cart.md?apiView=SDK> · <https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart/get-current-cart.md?apiView=SDK>
The cart's `_id` **is** the checkout id — pass it into the redirect session's `ecomCheckout.checkoutId`. Read the current cart, then hand its id to a redirect session, which carries the visitor/member session across to the hosted checkout on its own domain.
Doc: <https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/get-current-cart.md?apiView=SDK>

```js
const checkout = await currentCart.createCheckoutFromCurrentCart({ channelType: currentCart.ChannelType.WEB });
const { cart } = await currentCartV2.getCurrentCart(); // NOTE: returns { cart } — destructure it
const session = await redirects.createRedirectSession({
ecomCheckout: { checkoutId: checkout.checkoutId }, // checkout.checkoutId — NOT checkout._id
ecomCheckout: { checkoutId: cart._id }, // the cart's _id IS the checkout id
callbacks: { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` },
});
window.location.href = session.redirectSession.fullUrl; // the hosted-checkout URL
```

**⚠️ Return shapes are in the cheat-sheet** — `createCheckoutFromCurrentCart` gives **`checkout.checkoutId`** (a string), not `checkout._id`. Reading `checkout._id` (over-applying the `_id` rule) throws *"Cannot read properties of undefined (reading '_id')"* — the silent checkout crash.
**⚠️ The cart's `_id` is the checkout id.** Pass `cart._id` straight into the redirect session's `ecomCheckout.checkoutId`. And `getCurrentCart()` returns **`{ cart }`** — destructure it, or `cart` is `undefined` and `cart._id` throws *"Cannot read properties of undefined (reading '_id')"*.

**⚠️ CRITICAL: `origin` for `postFlowUrl`/`thankYouPageUrl` MUST be the `https://` published host — derive it from `window.location.origin`, NEVER `new URL(request.url).origin`.** The Headless redirect allowlist registers the site's **`https://`** host and treats **`http://<same host>` as a different, unlisted origin**. When the buyer returns from the hosted checkout (e.g. clicks "Continue Browsing"), the redirect goes through the allowlist — and an `http://` `postFlowUrl` **403s** with *"… isn't listed as an allowed redirect domain."* If you build the redirect session in a **server route** (`src/pages/api/*`), `new URL(request.url).origin` resolves to **`http://`** behind Wix's TLS-terminating proxy → guaranteed 403 on return. So **pass `window.location.origin` from the client** into the route (don't read the origin off the request), or force the scheme to `https`. Doc: <https://dev.wix.com/docs/go-headless/getting-started/setup/manage-urls/add-allowed-redirect-domains>.

### Formatting cart prices

**Product** prices from `productsV3` still carry a ready-to-show `actualPriceRange.minValue.formattedAmount` — use it directly. But **Cart V2 money does not**: every cart amount — line-item `pricing.unitPrice` / `pricing.totalPrice` **and** the `estimateCurrentCart`/`calculateCurrentCart` `summary.priceSummary.*` — is a `ConvertedMoney` `{ amount, convertedAmount }` with **no** formatted string. So once items are in the cart, you format the price yourself. The currency isn't on the money object; read it from the cart (`cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode`), and use `convertedAmount` (buyer's display currency) when present, else `amount` (site currency):

```js
function formatCartMoney(money, cart) {
const value = money?.convertedAmount ?? money?.amount;
const currency = cart?.customerInfo?.currencyCode ?? cart?.businessInfo?.currencyCode ?? 'USD';
return value == null ? '' : new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(Number(value));
}
// e.g. line item: formatCartMoney(item.pricing.totalPrice, cart)
// subtotal: formatCartMoney(estimate.summary.priceSummary.subtotal, cart)
```

Never hardcode `$` or assume USD — stores run in EUR/GBP too.

### Showing stock state

Read the **V3** inventory fields: product-level in-stock is `product.inventory.availabilityStatus` (`"IN_STOCK"`); variant-level is `variant.inventoryStatus.inStock`. Reading the V1 inventory field on V3 data returns `undefined` → everything renders out-of-stock (the all-OOS bug). These come from `productsV3` / `readOnlyVariantsV3`, not the V1 module.
Expand All @@ -208,7 +227,7 @@ function imgSrc(mediaMain, w = 600, h = 600) {

**Never hand-build a `static.wixstatic.com/.../v1/fit/...` URL** either — the format is easy to get wrong and the image then **403s**. Only `wix:image://` values need resolving; an already-absolute `https://` URL goes straight into `<img src>`. Doc: <https://dev.wix.com/docs/sdk/core-modules/sdk/media>

**This applies to cart line-item images too, not just product reads.** A cart `lineItem.image` is the same `wix:image://` identifier — run it through the same `imgSrc()` helper before `<img src>`. (If you build the cart over an API route, resolve there and return a ready URL so the component never sees a `wix:image://`.)
**This applies to cart line-item images too, not just product reads.** A cart `lineItem.attributes.image` is the same `wix:image://` identifier — run it through the same `imgSrc()` helper before `<img src>`. (If you build the cart over an API route, resolve there and return a ready URL so the component never sees a `wix:image://`.)

### Rendering product descriptions

Expand All @@ -230,7 +249,7 @@ Optional: render a `Product` schema.org JSON-LD `<script>` from the fetched prod

## Conclusion
A correct Catalog V3 storefront frontend:
- imports **`productsV3` / `readOnlyVariantsV3` / `categories` / `currentCart` / `redirects`** — never the V1 `products`/`collections` modules;
- imports **`productsV3` / `readOnlyVariantsV3` / `categories` / `currentCartV2` / `redirects`** — never the V1 `products`/`collections` modules;
- uses **`product._id`** (never `product.id`) as the cart's `catalogItemId`;
- resolves the **mandatory `variantId`** via `readOnlyVariantsV3` and passes it as `options.variantId` (not `options.options`);
- builds its category nav from a **live `categories.queryCategories()`** and filters category pages server-side with **`searchProducts` + `$matchItems: [{ id: categoryId }]`** keyed on the live `categoryId` — never a frozen seed-time `productIds` map, never `queryProducts` for category filtering, never `$hasSome`, never V1 `collectionIds`;
Expand Down
Loading
Loading