Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Never hardcode data the site owns. Every one of these was a real review finding:
- **Services** (names, durations, prices): render from `services.queryServices()`, keyed by real `_id` — no hardcoded "free"/"premium" tiers.
- **Forms**: render fields from `forms.getForm()` (labels, required flags, options, steps). Checkbox groups must render as checkboxes and submit **arrays** (`formData.getAll`), numbers as numbers, booleans as booleans.
- **Products**: query with explicit fields (`CURRENCY`, `PLAIN_DESCRIPTION`, `VARIANT_OPTION_CHOICE_NAMES`); resolve variants from `variantsInfo` — never blindly add `variants[0]` for a product with options.
- **Prices and totals**: display the SDK's `formattedAmount`; cart totals come from `estimateCurrentCartTotals()`/`priceSummary`. Never prefix `$` or do `parseFloat` money math — stores run in EUR/GBP too.
- **Prices and totals**: cart totals come from `currentCartV2.estimateCurrentCart()` / `cartV2.calculateCart()` → `summary.priceSummary`. In Cart V2 all cart money — line-item `pricing.unitPrice`/`totalPrice` **and** the summary's `subtotal`/`total` — is raw `ConvertedMoney` (`{ amount, convertedAmount }`) with **no** `formattedAmount`, so format it yourself with `Intl.NumberFormat` from the cart's `currencyCode`. (Other domains, e.g. Stores products, still expose `formattedAmount` — use it there.) Never prefix `$` or do `parseFloat` money math — stores run in EUR/GBP too.
- **Timezones**: query availability and format times in the visitor's timezone (`Intl.DateTimeFormat().resolvedOptions().timeZone`), never hardcoded UTC.
- **Blog content**: render Ricos via `RicosViewer` from `@wix/astro-ricos` (with `renameKeysFromSDKRequestToRESTRequest`), not a hand-rolled node renderer. Source per-post SEO tags (title/description/canonical/OG) from `post.seoData.tags`, falling back to title/excerpt/cover.
- Placeholder copy ("Business Name", template marketing text) is acceptable; placeholder *data* that shadows a Wix API is not.
Expand Down
7 changes: 5 additions & 2 deletions astro/commerce/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ A minimal Astro + React storefront wireframe backed by Wix Stores and Wix eComme
## How it connects to Wix

- **Catalog** — pages query products server-side with `@wix/stores` (`productsV3.queryProducts`).
- **Cart** — the React island uses `@wix/ecom` `currentCart` to add items, read the cart, and estimate totals.
- **Checkout** — `createCheckoutFromCurrentCart` plus `@wix/redirects` `createRedirectSession` sends the visitor to Wix Checkout.
- **Cart** — the React island uses `@wix/ecom` `currentCartV2` to add items, read the cart, and estimate totals.

> Migrating from Cart V1 / Checkout V1? This template is V2-only — see the migration guide: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/migration-guide

- **Checkout** — the cart id is the checkout id, so `@wix/redirects` `createRedirectSession` is given the current cart's id to send the visitor to Wix Checkout.
- **Members** — `@wix/members` reads the current member; login/logout go through the built-in `/api/auth/*` routes.
- **Media** — product images are scaled with `media.getScaledToFillImageUrl` from `@wix/sdk`.

Expand Down
2 changes: 1 addition & 1 deletion astro/commerce/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@wix/astro-pages": "^2.0.4",
"@wix/dashboard": "^1.3.36",
"@wix/data": "1.0.168",
"@wix/ecom": "1.0.806",
"@wix/ecom": "^1.0.806",
"@wix/essentials": "^1.0.6",
"@wix/members": "^1.0.496",
"@wix/redirects": "1.0.60",
Expand Down
66 changes: 45 additions & 21 deletions astro/commerce/src/components/AppIsland.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { currentCart } from '@wix/ecom';
import { cartV2, currentCartV2 } from '@wix/ecom';
import { redirects } from '@wix/redirects';
import { media } from '@wix/sdk';

// Public app id of the Wix Stores catalog, used in ecom catalog references.
const WIX_STORES_APP_ID = '215238eb-22a5-4c36-9e7b-e7c08025e04e';

// Cart V2 line-item prices are raw decimal strings (ConvertedMoney: { amount, convertedAmount }).
// Format them client-side from the amount + the cart's currency code.
function formatMoney(money, currencyCode) {
const value = money?.convertedAmount ?? money?.amount;
if (value == null) return '';
const num = Number(value);
if (Number.isNaN(num)) return '';
try {
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode || 'USD' }).format(num);
} catch {
return `${value}`;
}
}

function cartCurrency(cart) {
return cart?.customerInfo?.currencyCode ?? cart?.businessInfo?.currencyCode;
}

function lineItemCount(cart) {
return cart?.lineItems?.reduce((s, i) => s + (i.quantityInfo?.confirmedQuantity ?? 0), 0) ?? 0;
}

function imgSrc(mediaMain, w = 600, h = 600) {
const v = mediaMain?.image ?? mediaMain?.url ?? mediaMain;
if (!v) return '';
Expand Down Expand Up @@ -63,8 +85,8 @@ function ProductModal({ product, onClose, onAddedToCart }) {
if (!canAdd) return;
setAdding(true); setError('');
try {
await currentCart.addToCurrentCart({
lineItems: [{ quantity: 1, catalogReference: {
await currentCartV2.addLineItemsToCurrentCart({
catalogItems: [{ quantity: 1, catalogReference: {
catalogItemId: product._id,
appId: WIX_STORES_APP_ID,
...(selectedVariant?._id && { options: { variantId: selectedVariant._id } }),
Expand Down Expand Up @@ -119,12 +141,12 @@ function CartPanel({ onClose }) {

useEffect(() => {
Promise.all([
currentCart.getCurrentCart(),
currentCart.estimateCurrentCartTotals().catch(() => null),
currentCartV2.getCurrentCart(),
currentCartV2.estimateCurrentCart().catch(() => null),
])
.then(([c, estimate]) => {
setCart(c);
setPriceSummary(estimate?.priceSummary ?? null);
.then(([cartRes, estimate]) => {
setCart(cartRes?.cart ?? null);
setPriceSummary(estimate?.summary?.priceSummary ?? null);
})
.catch(() => setCart(null))
.finally(() => setLoading(false));
Expand All @@ -133,19 +155,21 @@ function CartPanel({ onClose }) {
async function handleCheckout() {
setCheckingOut(true);
try {
const checkout = await currentCart.createCheckoutFromCurrentCart({ channelType: 'WEB' });
// The cart id is the checkout id.
// We still use a redirect session so the visitor/member session carries across
// to the Wix-hosted checkout on its own domain.
const { cart } = await currentCartV2.getCurrentCart();
const session = await redirects.createRedirectSession({
ecomCheckout: { checkoutId: checkout.checkoutId },
ecomCheckout: { checkoutId: cart._id },
callbacks: { postFlowUrl: window.location.origin + '/', thankYouPageUrl: window.location.origin + '/' },
});
window.location.href = session.redirectSession.fullUrl;
} catch (e) { console.error(e); setCheckingOut(false); }
}

const itemCount = cart?.lineItems?.reduce((s, i) => s + (i.quantity ?? 0), 0) ?? 0;
const subtotal = priceSummary?.subtotal?.formattedConvertedAmount
?? priceSummary?.subtotal?.formattedAmount
?? '';
const itemCount = lineItemCount(cart);
// V2 summary money is ConvertedMoney (amount/convertedAmount, no formatted string) — format it client-side.
const subtotal = formatMoney(priceSummary?.subtotal, cartCurrency(cart));

return (
<div className="overlay right" onClick={onClose}>
Expand All @@ -158,14 +182,14 @@ function CartPanel({ onClose }) {
{loading && <p className="muted">Loading…</p>}
{!loading && !cart?.lineItems?.length && <p className="muted">Your cart is empty.</p>}
{cart?.lineItems?.map((item, i) => {
const s = imgSrc(item.image);
const price = item.price?.formattedConvertedAmount ?? item.price?.formattedAmount;
const s = imgSrc(item.attributes?.image);
const price = formatMoney(item.pricing?.totalPrice, cartCurrency(cart));
return (
<div key={i} className="cart-row">
<div key={item._id ?? i} className="cart-row">
{s ? <img className="cart-thumb" src={s} alt="" /> : <Ph className="cart-thumb" index={i} />}
<div className="grow">
<p className="cart-name">{item.productName?.translated ?? item.productName}</p>
<p className="cart-qty">Qty {item.quantity}</p>
<p className="cart-name">{item.name?.translated ?? item.name?.original}</p>
<p className="cart-qty">Qty {item.quantityInfo?.confirmedQuantity}</p>
</div>
{price && <p className="cart-name">{price}</p>}
</div>
Expand Down Expand Up @@ -302,8 +326,8 @@ export default function AppIsland({ products = [], member = null, page = 'home'

const refreshCart = useCallback(async () => {
try {
const cart = await currentCart.getCurrentCart();
setCartCount(cart?.lineItems?.reduce((s, i) => s + (i.quantity || 0), 0) ?? 0);
const { cart } = await currentCartV2.getCurrentCart();
setCartCount(lineItemCount(cart));
} catch { setCartCount(0); }
}, []);

Expand Down
2 changes: 1 addition & 1 deletion astro/scheduler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ An appointment scheduling template built with Astro and [Wix Bookings](https://d

- Listing bookable services with `@wix/bookings` (`services.queryServices`)
- Fetching availability in the visitor's timezone (`availabilityCalendar.queryAvailability`)
- Creating bookings for free services (`bookings.createBooking` + `@wix/ecom` checkout)
- Creating bookings for free services (`bookings.createBooking` + `@wix/ecom` cart)
- Redirecting to the Wix-hosted checkout for paid services (`@wix/redirects`)

The Wix integration logic lives in `src/utils/booking-service.ts`; pages are in `src/pages` (home, schedule, confirmation, 404).
Expand Down
41 changes: 24 additions & 17 deletions astro/scheduler/src/utils/booking-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { services, availabilityCalendar, bookings } from "@wix/bookings";
import { checkout } from "@wix/ecom";
import { cartV2 } from "@wix/ecom";
import { redirects } from "@wix/redirects";
import { BOOKINGS_APP_ID, TIME_FORMAT } from "./constants";

Expand Down Expand Up @@ -157,8 +157,23 @@ export async function createBooking(
},
});

const createdCheckout = await checkout.createCheckout({
lineItems: [
// Create a cart, calculate it to get the price-verification token, then
// place the order.
const createdCart = await cartV2.createCart({
cart: {
source: { channelType: "WEB" },
customerInfo: {
email: bookingData.email,
},
paymentInfo: {
billingContact: {
firstName: firstName,
lastName: lastName,
phone: bookingData.phone,
},
},
},
catalogItems: [
{
quantity: 1,
catalogReference: {
Expand All @@ -167,22 +182,14 @@ export async function createBooking(
},
},
],
channelType: checkout.ChannelType.WEB,
checkoutInfo: {
billingInfo: {
contactDetails: {
firstName: firstName,
lastName: lastName,
phone: bookingData.phone,
},
},
buyerInfo: {
email: bookingData.email,
},
},
});

await checkout.createOrder(createdCheckout._id!);
const cartId = createdCart._id!;
const calculated = await cartV2.calculateCart(cartId);
await cartV2.placeOrder(cartId, {
priceVerificationToken:
calculated.summary?.priceVerificationToken ?? undefined,
});

return booking;
} catch (error) {
Expand Down
14 changes: 8 additions & 6 deletions nextjs/commerce-ticketing/app/api/quick-buy/[productId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getRequestUrl } from '@app/utils/server-utils';
import { getWixClient } from '@app/hooks/useWixClientServer';
import { checkout as checkoutTypes } from '@wix/ecom';
import { STORES_APP_ID } from '@app/constants';

export async function GET(
Expand Down Expand Up @@ -52,14 +51,17 @@ export async function GET(
options: selectedOptions,
},
};
const checkout = await wixClient.ecomCheckout.createCheckout({
lineItems: [item],
channelType: checkoutTypes.ChannelType.WEB,
overrideCheckoutUrl: `${baseUrl}api/redirect-to-checkout?checkoutId={checkoutId}`,
// Create a fresh cart for this quick-buy. The cart id IS the checkout
// id, so we redirect straight from the created cart.
const cart = await wixClient.cartV2.createCart({
cart: {
customCheckoutUrl: `${baseUrl}api/redirect-to-checkout?checkoutId={checkout_id}`,
},
catalogItems: [item],
});

const { redirectSession } = await wixClient.redirects.createRedirectSession({
ecomCheckout: { checkoutId: checkout!._id! },
ecomCheckout: { checkoutId: cart!._id! },
callbacks: {
postFlowUrl: baseUrl,
thankYouPageUrl: `${baseUrl}stores-success`,
Expand Down
31 changes: 20 additions & 11 deletions nextjs/commerce-ticketing/app/components/Cart/CartView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,42 @@ import { useCart } from '@app/hooks/useCart';
import { useUI } from '@app/components/Provider/context';
import { useWixClient } from '@app/hooks/useWixClient';
import { Spinner } from 'flowbite-react';
import { currentCart } from '@wix/ecom';

export const CartView = ({ layout = 'mini' }: { layout?: 'full' | 'mini' }) => {
const wixClient = useWixClient();
const { closeSidebar, openModalNotPremium } = useUI();
const { data, isLoading } = useCart();
const [redirecting, setRedirecting] = useState<boolean>(false);
// Cart V2 currency lives on the cart, not on a top-level `currency` field.
const currencyCode =
data?.customerInfo?.currencyCode ?? data?.businessInfo?.currencyCode;
const subTotal = formatPrice(
data && {
amount:
data.lineItems?.reduce((acc, item) => {
// V2 `totalPrice` is already the line total (unit x quantity).
return (
acc +
Number.parseFloat(item.price?.amount ?? '0') * (item.quantity ?? 0)
Number(
item.pricing?.totalPrice?.convertedAmount ??
item.pricing?.totalPrice?.amount ??
0,
)
);
}, 0) ?? 0,
currencyCode: data.currency,
currencyCode,
}
);

const goToCheckout = useCallback(async () => {
closeSidebar();
setRedirecting(true);
try {
const checkout =
await wixClient.currentCart.createCheckoutFromCurrentCart({
channelType: currentCart.ChannelType.WEB,
});
// The cart id IS the checkout id.
const { cart } = await wixClient.currentCartV2.getCurrentCart();
const { redirectSession } =
await wixClient.redirects.createRedirectSession({
ecomCheckout: { checkoutId: checkout.checkoutId },
ecomCheckout: { checkoutId: cart!._id },
callbacks: {
postFlowUrl: window.location.origin,
thankYouPageUrl: `${window.location.origin}/stores-success`,
Expand All @@ -47,8 +52,12 @@ export const CartView = ({ layout = 'mini' }: { layout?: 'full' | 'mini' }) => {
window.location.href = redirectSession.fullUrl;
}
} catch (e: any) {
// In Cart V2 the "site must accept payments" gate is enforced at place-order on the
// Wix-hosted checkout — not on getCurrentCart/createRedirectSession — so a non-premium
// site surfaces the error there. This branch is kept as a best-effort fallback in case
// the SDK ever surfaces the code client-side.
if (
e.details.applicationError.code ===
e?.details?.applicationError?.code ===
'SITE_MUST_ACCEPT_PAYMENTS_TO_CREATE_CHECKOUT'
) {
openModalNotPremium();
Expand All @@ -58,7 +67,7 @@ export const CartView = ({ layout = 'mini' }: { layout?: 'full' | 'mini' }) => {
}, [
closeSidebar,
openModalNotPremium,
wixClient.currentCart,
wixClient.currentCartV2,
wixClient.redirects,
]);

Expand Down Expand Up @@ -110,7 +119,7 @@ export const CartView = ({ layout = 'mini' }: { layout?: 'full' | 'mini' }) => {
<CartItem
key={item._id}
item={item}
currencyCode={data?.currency!}
currencyCode={currencyCode!}
/>
))}
</ul>
Expand Down
5 changes: 3 additions & 2 deletions nextjs/commerce-ticketing/app/components/CartBag/CartBag.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client';
import { useUI } from '@app/components/Provider/context';
import { useCart } from '@app/hooks/useCart';
import { cart } from '@wix/ecom';
import { currentCartV2 } from '@wix/ecom';

export const CartBag = () => {
const { setSidebarView, toggleSidebar } = useUI();
const { data, isLoading } = useCart();
const itemsCount = !isLoading
? data?.lineItems?.reduce(
(count: number, item: cart.LineItem) => count + item.quantity!,
(count: number, item: currentCartV2.V2LineItem) =>
count + (item.quantityInfo?.confirmedQuantity ?? 0),
0
)
: 0;
Expand Down
Loading