@wix/agent-skills 1.13.0 → 1.15.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/README.md +4 -1
- package/package.json +1 -1
- package/skills/README.md +23 -0
- package/skills/wix-app/SKILL.md +10 -2
- package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +326 -0
- package/skills/wix-app/references/CODE_QUALITY.md +1 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +124 -115
- package/skills/wix-app/references/DASHBOARD_PAGE.md +3 -0
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +23 -9
- package/skills/wix-app/references/SITE_PLUGIN.md +1 -1
- package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +115 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +264 -0
- package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +37 -0
- package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +86 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +119 -0
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +103 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +76 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +82 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +89 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +124 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +91 -0
- package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +71 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +70 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +73 -0
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +98 -0
- package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +278 -0
- package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +94 -0
- package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +43 -0
- package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +66 -0
- package/skills/wix-app/references/auto-patterns-dashboard/views.md +76 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -14
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +54 -29
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +2 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +17 -105
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +127 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +2 -1
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +15 -15
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +25 -53
- package/skills/wix-app/scripts/generate-auto-patterns.js +510 -0
- package/skills/wix-auth/SKILL.md +12 -0
- package/skills/wix-auth/references/device-flow.md +140 -0
- package/skills/wix-docs/SKILL.md +188 -0
- package/skills/wix-docs/references/API_SPEC_SEARCH.md +213 -0
- package/skills/wix-docs/references/EXTRACTING.md +120 -0
- package/skills/wix-headless/SKILL.md +84 -130
- package/skills/wix-headless/entry/bootstrap.mjs +93 -0
- package/skills/wix-headless/entry/skill.md +78 -0
- package/skills/wix-headless/references/AI_FEATURES.md +178 -0
- package/skills/wix-headless/references/CAPABILITIES.md +84 -0
- package/skills/wix-headless/references/DISCOVERY.md +34 -56
- package/skills/wix-headless/references/DOC_DISCOVERY.md +37 -0
- package/skills/wix-headless/references/FEEDBACK.md +118 -0
- package/skills/wix-headless/references/IMAGE_GENERATION.md +69 -0
- package/skills/wix-headless/references/SDK_HANDOFF.md +97 -0
- package/skills/wix-headless/references/SEED.md +44 -188
- package/skills/wix-headless/references/SETUP.md +32 -103
- package/skills/wix-headless/references/astro.md +98 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +196 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +236 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-bookings.md +279 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-cms.md +163 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-events.md +203 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +222 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +160 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +118 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-portfolio.md +132 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +124 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +188 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +173 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurants.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-blog.md +175 -0
- package/skills/wix-headless/references/inline-recipes/setup-bookings.md +228 -0
- package/skills/wix-headless/references/inline-recipes/setup-cms.md +200 -0
- package/skills/wix-headless/references/inline-recipes/setup-events.md +196 -0
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +174 -0
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +176 -0
- package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +153 -0
- package/skills/wix-headless/references/inline-recipes/setup-pricing-plans.md +229 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +161 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +117 -0
- package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +152 -0
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +57 -0
- package/skills/wix-headless/references/managed/CONNECT.md +74 -0
- package/skills/wix-headless/references/managed/CREATE.md +130 -0
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +57 -0
- package/skills/wix-headless/references/non-astro.md +59 -0
- package/skills/wix-headless/references/self-managed/AUTHENTICATION.md +22 -0
- package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +7 -0
- package/skills/wix-headless/references/stripe/AUTHENTICATION.md +55 -0
- package/skills/wix-headless/references/stripe/DEPLOYMENT.md +63 -0
- package/skills/wix-manage/SKILL.md +24 -1
- package/skills/wix-manage/references/analytics/query-site-analytics.md +241 -0
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +1 -1
- package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +265 -0
- package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +436 -0
- package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +209 -0
- package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +249 -0
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +51 -0
- package/skills/wix-vibe-headless/SKILL.md +136 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +128 -0
- package/skills/wix-vibe-headless/references/blog/wix-blog.js +217 -0
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +153 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-checkout.js +147 -0
- package/skills/wix-vibe-headless/references/bookings/wix-bookings-services.js +215 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +154 -0
- package/skills/wix-vibe-headless/references/cms/wix-cms.js +186 -0
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +142 -0
- package/skills/wix-vibe-headless/references/events/wix-events-browse.js +135 -0
- package/skills/wix-vibe-headless/references/events/wix-events-registration.js +187 -0
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +211 -0
- package/skills/wix-vibe-headless/references/members/wix-members-auth.js +387 -0
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +111 -0
- package/skills/wix-vibe-headless/references/portfolio/wix-portfolio.js +194 -0
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +127 -0
- package/skills/wix-vibe-headless/references/pricing-plans/wix-pricing-plans.js +157 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +138 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-menu.js +224 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-ordering.js +156 -0
- package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-reservations.js +107 -0
- package/skills/wix-vibe-headless/references/shared/wix-client.js +186 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +155 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-cart.js +147 -0
- package/skills/wix-vibe-headless/references/storefront/wix-store-catalog.js +142 -0
- package/skills/wix-headless/references/BUILD-astro.md +0 -244
- package/skills/wix-headless/references/BUILD-own-build.md +0 -167
- package/skills/wix-headless/references/BUILD.md +0 -105
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +0 -165
- package/skills/wix-headless/references/DISCOVERY-connect.md +0 -55
- package/skills/wix-headless/references/DISCOVERY-create.md +0 -238
- package/skills/wix-headless/references/PLAN-connect.md +0 -19
- package/skills/wix-headless/references/PLAN-create.md +0 -17
- package/skills/wix-headless/references/PLAN.md +0 -92
- package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +0 -300
- package/skills/wix-headless/references/astro/blog/BLOG_SETUP.md +0 -218
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +0 -45
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +0 -77
- package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +0 -214
- package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +0 -231
- package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +0 -453
- package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +0 -354
- package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +0 -299
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +0 -351
- package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +0 -119
- package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +0 -103
- package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +0 -132
- package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +0 -463
- package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +0 -119
- package/skills/wix-headless/references/astro/gift-cards/PAGES.md +0 -129
- package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +0 -178
- package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +0 -85
- package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +0 -129
- package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +0 -229
- package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +0 -215
- package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +0 -165
- package/skills/wix-headless/references/astro/templates/Footer.astro +0 -27
- package/skills/wix-headless/references/astro/templates/Layout.astro +0 -132
- package/skills/wix-headless/references/astro/templates/Navigation.astro +0 -38
- package/skills/wix-headless/references/astro/templates/astro.config.mjs +0 -41
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +0 -374
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +0 -136
- package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +0 -25
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +0 -71
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +0 -54
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +0 -49
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +0 -245
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +0 -424
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +0 -176
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +0 -181
- package/skills/wix-headless/references/astro/templates/ecom/CartBadge.tsx +0 -84
- package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +0 -543
- package/skills/wix-headless/references/astro/templates/ecom/cart.astro +0 -13
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +0 -302
- package/skills/wix-headless/references/astro/templates/ecom/discounts.ts +0 -71
- package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +0 -24
- package/skills/wix-headless/references/astro/templates/gift-cards/GiftCardPurchase.tsx +0 -172
- package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +0 -42
- package/skills/wix-headless/references/astro/templates/gift-cards/_nav-snippet.astro +0 -11
- package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +0 -220
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +0 -51
- package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.ts +0 -75
- package/skills/wix-headless/references/astro/templates/global.css +0 -288
- package/skills/wix-headless/references/astro/templates/index.astro +0 -56
- package/skills/wix-headless/references/astro/templates/stores/AddToCartButton.tsx +0 -77
- package/skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx +0 -176
- package/skills/wix-headless/references/astro/templates/stores/CategoryRail.astro +0 -69
- package/skills/wix-headless/references/astro/templates/stores/ProductCard.astro +0 -79
- package/skills/wix-headless/references/astro/templates/stores/ProductPurchase.tsx +0 -397
- package/skills/wix-headless/references/astro/templates/stores/SeoTags.astro +0 -21
- package/skills/wix-headless/references/astro/templates/stores/back-in-stock.ts +0 -67
- package/skills/wix-headless/references/astro/templates/stores/categories.ts +0 -155
- package/skills/wix-headless/references/astro/templates/stores/category/[slug].astro +0 -130
- package/skills/wix-headless/references/astro/templates/stores/components-stores.css +0 -400
- package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +0 -128
- package/skills/wix-headless/references/astro/templates/stores/products/index.astro +0 -101
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +0 -161
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +0 -32
- package/skills/wix-headless/references/bookings/FLOW.md +0 -246
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +0 -95
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +0 -355
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +0 -176
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +0 -71
- package/skills/wix-headless/references/commands/known-apps.json +0 -6
- package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +0 -142
- package/skills/wix-headless/references/custom/INSTRUCTIONS.md +0 -88
- package/skills/wix-headless/references/custom/blog/WIRING.md +0 -52
- package/skills/wix-headless/references/custom/bookings/WIRING.md +0 -197
- package/skills/wix-headless/references/custom/cms/WIRING.md +0 -97
- package/skills/wix-headless/references/custom/ecom/WIRING.md +0 -67
- package/skills/wix-headless/references/custom/forms/WIRING.md +0 -101
- package/skills/wix-headless/references/custom/stores/WIRING.md +0 -80
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +0 -59
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +0 -43
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +0 -51
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +0 -357
- package/skills/wix-headless/references/seed-recipes.md +0 -49
- package/skills/wix-headless/references/shared/AGENTS_MD.md +0 -47
- package/skills/wix-headless/references/shared/AUTHENTICATION.md +0 -108
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +0 -8
- package/skills/wix-headless/references/shared/DESIGN_MD.md +0 -84
- package/skills/wix-headless/references/shared/DOCS_SEARCH.md +0 -41
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +0 -186
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +0 -206
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +0 -97
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +0 -143
- package/skills/wix-headless/references/shared/STYLING.md +0 -141
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +0 -126
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +0 -252
- package/skills/wix-headless/references/verticals/_schema.json +0 -74
- package/skills/wix-headless/references/verticals/_schema.md +0 -95
- package/skills/wix-headless/references/verticals/blog.md +0 -31
- package/skills/wix-headless/references/verticals/bookings.md +0 -41
- package/skills/wix-headless/references/verticals/cms.md +0 -30
- package/skills/wix-headless/references/verticals/ecom.md +0 -25
- package/skills/wix-headless/references/verticals/forms.md +0 -26
- package/skills/wix-headless/references/verticals/gift-cards.md +0 -24
- package/skills/wix-headless/references/verticals/stores.md +0 -33
- package/skills/wix-headless/scripts/check-manifest.mjs +0 -335
- package/skills/wix-headless/scripts/compose.mjs +0 -525
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -157
- package/skills/wix-headless/scripts/flatten.sh +0 -61
- package/skills/wix-headless/scripts/generate-images.mjs +0 -242
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +0 -148
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +0 -190
- package/skills/wix-headless/scripts/scaffold.sh +0 -139
- package/skills/wix-headless/scripts/seed-utilities.sh +0 -93
- package/skills/wix-headless/scripts/write-decorative-json.mjs +0 -62
- package/skills/wix-headless/shared-utilities/analytics.ts +0 -40
- package/skills/wix-headless/shared-utilities/ricos.ts +0 -179
- package/skills/wix-headless/shared-utilities/wix-image.ts +0 -72
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "How to Code a Store"
|
|
3
|
+
description: The frontend read/cart contract for a Catalog V3 storefront — which SDK modules to import, how to resolve the mandatory variantId, the exact add-to-cart shape, how to filter by category, and how to read inventory. Specifies the *how* (modules + exact calls + the failure modes the docs omit); which products/categories to render come from the catalog the storefront reads.
|
|
4
|
+
---
|
|
5
|
+
**RECIPE**: How to Code a Wix Online Store Frontend (Catalog V3 + eCommerce cart)
|
|
6
|
+
|
|
7
|
+
A concise contract for writing the **frontend code** of a storefront against a Catalog V3 store: listing products, filtering by category, adding to cart, and checking out. **This recipe is the *how* (which modules, which calls, which fields), not the *what*** — which products to show, how the page looks, and the framework are decided by the request you're fulfilling.
|
|
8
|
+
|
|
9
|
+
> **This recipe is for CODING the storefront, not for seeding it.** It assumes a Catalog V3 store already exists (products, variants, categories, inventory). It says nothing about creating products — only how to read and purchase them from frontend code.
|
|
10
|
+
|
|
11
|
+
> **⚠️ Reading rule — always append `.md?apiView=SDK` to every doc link below.** The Wix docs render two views of the same page. The **bare / REST view shows `id`**; the **`?apiView=SDK` view shows `_id`** — and the SDK is what your frontend calls. Reading the REST view by mistake is the single most common source of the cart-killing `product.id` bug (see the `_id` rule under *Listing products*). Fetch the `.md?apiView=SDK` form directly; don't re-discover these with search.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The modules and the client (read this first)
|
|
16
|
+
|
|
17
|
+
**Stores app id** (a constant you will need for the cart's `catalogReference`):
|
|
18
|
+
`215238eb-22a5-4c36-9e7b-e7c08025e04e`
|
|
19
|
+
|
|
20
|
+
**⚠️ CRITICAL: use the V3 SDK modules, never the V1 ones.** The store is seeded with Catalog **V3** data. The legacy V1 `products` / `collections` modules read a different shape against the same data and fail in ways the SDK swallows silently (empty category pages, unresolved variants, `400`s on server-side filters). Import only:
|
|
21
|
+
|
|
22
|
+
| Need | Package | Module |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Products (list, get, search, filter) | `@wix/stores` | `productsV3` |
|
|
25
|
+
| Variants (to resolve `variantId`) | `@wix/stores` | `readOnlyVariantsV3` |
|
|
26
|
+
| Categories | `@wix/stores` | `categories` |
|
|
27
|
+
| Cart (add / get / checkout) | `@wix/ecom` | `currentCart` |
|
|
28
|
+
| Redirect to hosted checkout | `@wix/redirects` | `redirects` |
|
|
29
|
+
|
|
30
|
+
**Never** import the V1 `products` or `collections` modules from `@wix/stores`.
|
|
31
|
+
|
|
32
|
+
**Auth / client — framework split:**
|
|
33
|
+
- **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`.**
|
|
34
|
+
- **Non-Astro (Vite/React/Vue/static):** build one manual visitor client and reuse it:
|
|
35
|
+
```js
|
|
36
|
+
import { createClient, OAuthStrategy } from '@wix/sdk';
|
|
37
|
+
import { productsV3, readOnlyVariantsV3 } from '@wix/stores';
|
|
38
|
+
import { currentCart } from '@wix/ecom';
|
|
39
|
+
import { redirects } from '@wix/redirects';
|
|
40
|
+
|
|
41
|
+
const client = createClient({
|
|
42
|
+
modules: { productsV3, readOnlyVariantsV3, currentCart, redirects },
|
|
43
|
+
auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
The `clientId` is public, not a secret.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## The shapes you read (field cheat-sheet)
|
|
51
|
+
|
|
52
|
+
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`).
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
// productsV3.queryProducts() / .searchProducts() → result.items[]
|
|
56
|
+
product = {
|
|
57
|
+
_id, // links · cart catalogItemId · variant filter (NOT .id → empty → HTTP 500)
|
|
58
|
+
slug, name, visible, // only visible:true is returned to a visitor token
|
|
59
|
+
actualPriceRange: { minValue: { amount } }, // PRICE (NOT price.actualPrice.amount — that's the seed/WRITE shape → $NaN)
|
|
60
|
+
compareAtPriceRange: { minValue: { amount } }, // strike-through price
|
|
61
|
+
inventory: { availabilityStatus }, // product-level stock, "IN_STOCK"
|
|
62
|
+
media: { main: { image } }, // image is a wix:image:// id → resolve it (see Rendering images)
|
|
63
|
+
plainDescription, // plain string; description.nodes is the rich-text form
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// readOnlyVariantsV3.queryVariants().eq('productData.productId', product._id).find() → result.items[]
|
|
67
|
+
// variants are a SEPARATE resource — queryProducts / getProduct return variantsInfo: null
|
|
68
|
+
variant = {
|
|
69
|
+
variantId, // → cart options.variantId (use variant.variantId ?? variant._id)
|
|
70
|
+
optionChoices: [{ optionChoiceNames: { optionName, choiceName } }], // match the buyer's Size/Color selection
|
|
71
|
+
inventoryStatus: { inStock }, // variant-level stock (boolean)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// currentCart.getCurrentCart() → { lineItems: [...] }
|
|
75
|
+
lineItem = { quantity, price: { amount }, image } // price is HERE (NOT actualPriceRange); image is wix:image:// too → resolve
|
|
76
|
+
|
|
77
|
+
// currentCart.createCheckoutFromCurrentCart({ channelType }) → { checkoutId } // a STRING — NOT { checkout }, NOT _id
|
|
78
|
+
// redirects.createRedirectSession({ ecomCheckout: { checkoutId }, callbacks }) → { redirectSession: { fullUrl } }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## The storefront features (build the ones the site needs)
|
|
84
|
+
|
|
85
|
+
Each section below is a **self-contained storefront feature** — implement only the ones the site uses; they don't have to be built in order, and some sites need just a few of them. The only ordering is *within* a feature (e.g. resolve the variant before adding it to the cart).
|
|
86
|
+
|
|
87
|
+
### Listing products (and the `_id` rule)
|
|
88
|
+
|
|
89
|
+
Query products with `productsV3.queryProducts()` / `.searchProducts()`.
|
|
90
|
+
Doc: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/query-products.md?apiView=SDK>
|
|
91
|
+
|
|
92
|
+
**⚠️ `queryProducts()` takes NO arguments — it returns a builder, not a Promise.** Chain `.eq(...)`/`.limit(...)`/`.find()` on it (e.g. `await productsV3.queryProducts().limit(50).find()`). Passing a query object (`queryProducts({...})`) does **not** type-check and returns a `Promise` that has no `.limit`/`.eq`/`.find` to chain — if the SDK reference page shows a `(query, options)→Promise` signature, trust the installed no-arg builder over that example. (`searchProducts` is the one that *does* take a query — see category filtering below.)
|
|
93
|
+
|
|
94
|
+
**⚠️ 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`.)
|
|
95
|
+
|
|
96
|
+
**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`.
|
|
97
|
+
|
|
98
|
+
**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.
|
|
99
|
+
|
|
100
|
+
**Price** comes from `actualPriceRange.minValue.amount` (see the cheat-sheet) — **not** `price.actualPrice.amount` (the seed/write shape), which reads `undefined` → `$NaN`.
|
|
101
|
+
|
|
102
|
+
### Category navigation — list categories live
|
|
103
|
+
|
|
104
|
+
**Build the category nav/rail from a live `categories` query (`@wix/categories`), never from a seeded category list** — a category the owner adds later then self-registers in the nav with no code change. Query at render time and read each `{ _id, name, slug }`; render the bar only when it returns **more than one** category, and treat it as **non-fatal** (wrap in try/catch, render without the bar if it fails).
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { categories } from '@wix/categories';
|
|
108
|
+
const res = await categories.queryCategories({
|
|
109
|
+
treeReference: { appNamespace: '@wix/stores' },
|
|
110
|
+
}).exists('name', true).find(); // read the returned array per the SDK doc; ids are `_id`; link each to /category/<slug>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**⚠️ The query MUST carry a filter condition — chain `.exists('name', true)` (as above), do NOT call a bare `.find()`.** A `.find()` with no chained filter serializes an empty `"filter": {}`, which `categories/v1/categories/query` rejects with `400 INVALID_FILTER` ("Filter expression cannot contain an empty condition"). **This is fatal on the visitor/manual-client (non-Astro) path** — the call throws and the nav never renders; managed-Astro's server-side transport happens to tolerate the empty filter, so a bare `.find()` *looks* fine there but breaks the moment the same code runs client-side. `.exists('name', true)` is a tautology (every category has a `name`), so it matches **all** categories and is accepted on **both** paths — use this one shape everywhere. **Do NOT filter on `visible`** (`.eq('visible', true)`): unlike `name`, `visible` is **not declared filterable** on `queryCategories`, so it triggers a silently-swallowed `400` and the nav renders blank; if you ever need to hide a category, filter the returned array **client-side**. (`getCategoryBySlug(slug, { appNamespace: '@wix/stores' })` on the category page is unaffected — it takes no query filter.) This is a **distinct API from bookings' `categoriesV2`** — don't copy that module's query shape here; read the exact result-array key + field names from the `@wix/categories` SDK doc: <https://dev.wix.com/docs/sdk/business-solutions/categories/introduction.md?apiView=SDK>.
|
|
114
|
+
|
|
115
|
+
### Filtering products by category
|
|
116
|
+
|
|
117
|
+
**Filter server-side by the live `categoryId` — keyed on the stable category id, so products the owner adds to a category later appear with no code change or re-publish.** This is the **prescribed** approach; do **not** freeze a seed-time `category→productIds` map into the code and filter client-side against it (a product added to that category in the backoffice would never appear on its category page — the exact "owner edit is lost" failure this recipe exists to prevent).
|
|
118
|
+
|
|
119
|
+
**⚠️ CRITICAL: category filtering MUST use `searchProducts`, NOT `queryProducts`.** `directCategoriesInfo.categories` is **not declared as filterable in `queryProducts`** — passing it there returns HTTP `400 "... is not declared as filterable"`, which the SDK **swallows silently**, leaving an empty category page that looks like "no products". This is the #1 way this breaks. Use Search Products:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const { items } = await productsV3.searchProducts({
|
|
123
|
+
filter: { 'directCategoriesInfo.categories': { $matchItems: [{ id: categoryId }] } },
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **`categoryId`** is the stable id from the live `categories.queryCategories()` result above (a category's `_id`) — read it from the render context, never a hardcoded seed-time id list.
|
|
128
|
+
- **Method:** `searchProducts`, never `queryProducts` (the field is only filterable in search).
|
|
129
|
+
- **Operator:** `$matchItems`, never `$hasSome` (the natural-looking guess returns nothing).
|
|
130
|
+
- **Inner key:** `id` (the category GUID), inside `$matchItems: [{ id: … }]`.
|
|
131
|
+
- **Never** the V1 `collectionIds` / `collections.id` paths — they return empty against V3 data.
|
|
132
|
+
|
|
133
|
+
Docs: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md?apiView=SDK> · <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/supported-filters-and-sorting.md?apiView=SDK>
|
|
134
|
+
|
|
135
|
+
### Adding to cart — the V3 cart contract
|
|
136
|
+
|
|
137
|
+
Adding to cart is two parts of **one feature**: resolve the variant first, then add it. The variant resolution is not a standalone concern — it exists only to feed the add call.
|
|
138
|
+
|
|
139
|
+
**1 · Resolve the `variantId` (mandatory).** Variants are a **separate read-only resource** — `queryProducts` / `getProduct` do **not** return variant data (this is documented; `variantsInfo` comes back `null`). Resolve the variant yourself:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const { items } = await readOnlyVariantsV3
|
|
143
|
+
.queryVariants()
|
|
144
|
+
.eq('productData.productId', product._id) // NOTE: productData.productId is the filter field
|
|
145
|
+
.find();
|
|
146
|
+
```
|
|
147
|
+
Doc: <https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/read-only-variants-v3/query-variants.md?apiView=SDK>
|
|
148
|
+
|
|
149
|
+
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`**.
|
|
150
|
+
|
|
151
|
+
**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>
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
await currentCart.addToCurrentCart({
|
|
155
|
+
lineItems: [{
|
|
156
|
+
quantity,
|
|
157
|
+
catalogReference: {
|
|
158
|
+
catalogItemId: product._id, // the product's _id (the `_id` rule above)
|
|
159
|
+
appId: '215238eb-22a5-4c36-9e7b-e7c08025e04e',
|
|
160
|
+
options: { variantId }, // the resolved variantId from part 1
|
|
161
|
+
},
|
|
162
|
+
}],
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**⚠️ 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).
|
|
167
|
+
|
|
168
|
+
**⚠️ 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).
|
|
169
|
+
|
|
170
|
+
### Checkout
|
|
171
|
+
|
|
172
|
+
Create a checkout from the current cart, then redirect the buyer to the hosted checkout.
|
|
173
|
+
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>
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
const checkout = await currentCart.createCheckoutFromCurrentCart({ channelType: currentCart.ChannelType.WEB });
|
|
177
|
+
const session = await redirects.createRedirectSession({
|
|
178
|
+
ecomCheckout: { checkoutId: checkout.checkoutId }, // checkout.checkoutId — NOT checkout._id
|
|
179
|
+
callbacks: { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` },
|
|
180
|
+
});
|
|
181
|
+
window.location.href = session.redirectSession.fullUrl; // the hosted-checkout URL
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**⚠️ 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.
|
|
185
|
+
|
|
186
|
+
**⚠️ 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>.
|
|
187
|
+
|
|
188
|
+
### Showing stock state
|
|
189
|
+
|
|
190
|
+
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.
|
|
191
|
+
|
|
192
|
+
### Rendering product images
|
|
193
|
+
|
|
194
|
+
Product media may come back as a **`wix:image://v1/<hash>/<file>#originWidth=…` identifier, not a ready URL** — this is what the SDK returns for images stored in Wix Media (e.g. once brand imagery is attached). Putting that string straight into `<img src>` shows nothing. `media.main` may carry **either** an already-absolute `.url` (e.g. an Unsplash placeholder seeded when imagery is off) **or** an `.image` that is a `wix:image://` id needing resolution — handle both with **one** helper and reuse it on every page that renders an image:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { media } from '@wix/sdk';
|
|
198
|
+
function imgSrc(mediaMain, w = 600, h = 600) {
|
|
199
|
+
const v = mediaMain?.image ?? mediaMain?.url ?? mediaMain; // the value can be a string or {url}
|
|
200
|
+
if (!v) return '';
|
|
201
|
+
if (typeof v === 'string' && v.startsWith('wix:image://')) return media.getScaledToFillImageUrl(v, w, h);
|
|
202
|
+
return typeof v === 'string' ? v : (v.url ?? ''); // already an absolute https URL
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**⚠️ Do NOT write `m.url ?? m.image` (or `image?.url ?? image`).** That returns the bare **`wix:image://` string** whenever `.url` is absent — which is exactly the Wix-Media case — and the browser fails it with **`ERR_UNKNOWN_URL_SCHEME`** (blank image). The `wix:image://` branch must go through `media.getScaledToFillImageUrl`; never return it raw. Define the helper **once** and call it from every render path (home, listing, product, cart) — resolving on some pages but not others is the common partial failure.
|
|
207
|
+
|
|
208
|
+
**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>
|
|
209
|
+
|
|
210
|
+
**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://`.)
|
|
211
|
+
|
|
212
|
+
### Rendering product descriptions
|
|
213
|
+
|
|
214
|
+
Don't print the raw node object. A product description is rich text (`description.nodes`). Render the rich-text nodes, or use `plainDescription` for a plain string. Printing the raw node object dumps literal `<p>…</p>` into the page.
|
|
215
|
+
|
|
216
|
+
### SEO on item pages (Astro, Wix-managed)
|
|
217
|
+
|
|
218
|
+
A **product detail** page is a Wix **item page**: its `<title>`/description/OG/canonical come from what the owner sets in the dashboard. On the Astro (Wix-managed) frontend, wire it per the canonical guide — **[Add SEO Support to Item Pages](https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/add-seo-support-to-item-pages.md)** — which covers the three steps: export `wixMetadata` (registers the route → sitemap + dashboard SEO editor), call `loadSEOTagsServiceConfig(...)`, and render `<SEO.Tags>` (from `@wix/seo`; deps + `@wix/essentials ≥ 1.0.10` are in the guide's "Before you begin").
|
|
219
|
+
|
|
220
|
+
For a product page use:
|
|
221
|
+
- **`wixMetadata`** from `WIX_APPS.checkoutAndOrders.productPageMetadata` — referenced **directly** in the export (module scope). ⚠️ It's `WIX_APPS.checkoutAndOrders`, **not** `WIX_APPS.stores` (`stores.id` is the catalog id for `catalogReference`, a different value). The `identifiers` key is your route param; the token is `…productPageMetadata.identifiers.handle`.
|
|
222
|
+
- **`itemType`**: `seoTags.ItemType.STORES_PRODUCT`.
|
|
223
|
+
|
|
224
|
+
**If you build a dedicated category route** (e.g. `/category/[slug]` or `/search/[collection]`), wire it the same way with `WIX_APPS.checkoutAndOrders.categoryPageMetadata` + `seoTags.ItemType.STORES_CATEGORY`. (A category rendered only as a query-string *filter* on the products listing is a main page — it gets its SEO from automatic injection, no `wixMetadata` needed.)
|
|
225
|
+
|
|
226
|
+
Optional: render a `Product` schema.org JSON-LD `<script>` from the fetched product for rich results (see the guide's structured-data step).
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Conclusion
|
|
231
|
+
A correct Catalog V3 storefront frontend:
|
|
232
|
+
- imports **`productsV3` / `readOnlyVariantsV3` / `categories` / `currentCart` / `redirects`** — never the V1 `products`/`collections` modules;
|
|
233
|
+
- uses **`product._id`** (never `product.id`) as the cart's `catalogItemId`;
|
|
234
|
+
- resolves the **mandatory `variantId`** via `readOnlyVariantsV3` and passes it as `options.variantId` (not `options.options`);
|
|
235
|
+
- 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`;
|
|
236
|
+
- reads inventory from the **V3** shape and renders rich-text descriptions, not raw nodes.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "How to Code Bookings"
|
|
3
|
+
description: The frontend read/booking contract for a Wix Bookings (Services V2) site — which SDK modules to import, how to list services, fetch appointment vs class availability, render the schema-driven booking form, and run the createBooking → ecom Cart V2 → checkout-or-place sequence. Specifies the *how* (modules + exact calls + the failure modes the docs omit); which services to render and how the page looks come from the request.
|
|
4
|
+
---
|
|
5
|
+
**RECIPE**: How to Code a Wix Bookings Frontend (Services V2 + ecom Cart V2 checkout)
|
|
6
|
+
|
|
7
|
+
A concise contract for writing the **frontend code** of a Bookings site: listing services, picking an availability slot, collecting the booking form, and completing the booking. **This recipe is the *how* (which modules, which calls, which fields), not the *what*** — which services to show, how the page looks, and the framework are decided by the request you're fulfilling.
|
|
8
|
+
|
|
9
|
+
> **This recipe is for CODING the booking flow, not for seeding it.** It assumes a Services V2 backend already exists (services with a category, staff resources, durations/prices, and — for classes — scheduled sessions). It says nothing about creating services — only how to read and book them from frontend code.
|
|
10
|
+
|
|
11
|
+
> **⚠️ Reading rule — always append `.md?apiView=SDK` to every doc link below.** The Wix docs render two views of the same page. The **bare / REST view shows `id`**; the **`?apiView=SDK` view shows `_id`** — and the SDK is what your frontend calls. Reading the REST view by mistake is the most common source of the `service.id`-is-`undefined` bug. If a field name surprises you, you're probably reading the REST view — re-open it with `?apiView=SDK`. Discover any shape not pinned here with `SearchWixSDKDocumentation`, not by guessing a URL.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The modules and the client (read this first)
|
|
16
|
+
|
|
17
|
+
**Constants** (a frontend file, e.g. `src/services/constants.ts`):
|
|
18
|
+
- **Wix Bookings app id** (the cart's `catalogReference.appId`): `13d21c63-b5ec-5912-8397-c3a5ddb27a97`
|
|
19
|
+
- **Staff-member resource type id** (the ANY_RESOURCE fallback + staff filtering): `1cd44cf8-756f-41c3-bd90-3e2ffcaf1155`
|
|
20
|
+
|
|
21
|
+
**⚠️ CRITICAL: there is NO `availability` namespace.** `import { availability }` / `availability.queryAvailability` does not exist and fails. Appointment availability is `availabilityTimeSlots.listAvailabilityTimeSlots`; class availability is `eventTimeSlots.listEventTimeSlots` — two different namespaces. Import only:
|
|
22
|
+
|
|
23
|
+
| Need | Package | Module |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Services (list, query by slug) | `@wix/bookings` | `services` |
|
|
26
|
+
| Appointment availability | `@wix/bookings` | `availabilityTimeSlots` |
|
|
27
|
+
| Class availability | `@wix/bookings` | `eventTimeSlots` |
|
|
28
|
+
| Categories (optional filter) | `@wix/bookings` | `categoriesV2` |
|
|
29
|
+
| Locations (optional filter) | `@wix/bookings` | `services` (`queryLocations`) |
|
|
30
|
+
| Create the booking | `@wix/bookings` | `bookings` (`createBooking`) |
|
|
31
|
+
| Cart that holds the seat (add / calc / place) | `@wix/auto_sdk_ecom_cart-v-2` | `createCart`, `calculateCart`, `placeOrder` |
|
|
32
|
+
| Redirect to hosted checkout | `@wix/redirects` | `redirects` |
|
|
33
|
+
| Booking-form schema | `@wix/forms` | `forms` (`getFormSummary`) |
|
|
34
|
+
|
|
35
|
+
**Never** use `confirmBooking` (see *Creating the booking*) — the ecom cart confirms the seat here, not a server-side confirm.
|
|
36
|
+
|
|
37
|
+
**Auth / client — framework split:**
|
|
38
|
+
- **Astro (Wix-managed):** authentication is ambient. Call the modules directly from server components / backend routes and from browser islands (the `@wix/astro` visitor client) — **no `createClient`, no `OAuthStrategy`, no `clientId`.** Nothing here needs elevation (the reads below all run as the visitor); a public-env `clientId` read in `.astro` SSR is `undefined` at server render → 500, so don't build an `OAuthStrategy` client there.
|
|
39
|
+
- **Non-Astro (Vite/React/Vue/static):** build one manual visitor client and reuse it:
|
|
40
|
+
```js
|
|
41
|
+
import { createClient, OAuthStrategy } from '@wix/sdk';
|
|
42
|
+
import { services, availabilityTimeSlots, eventTimeSlots, categoriesV2, bookings } from '@wix/bookings';
|
|
43
|
+
import { createCart, calculateCart, placeOrder } from '@wix/auto_sdk_ecom_cart-v-2';
|
|
44
|
+
import { redirects } from '@wix/redirects';
|
|
45
|
+
import { forms } from '@wix/forms';
|
|
46
|
+
|
|
47
|
+
const client = createClient({
|
|
48
|
+
modules: { services, availabilityTimeSlots, eventTimeSlots, categoriesV2, bookings, createCart, calculateCart, placeOrder, redirects, forms },
|
|
49
|
+
auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
The `clientId` is public, not a secret. A mis-wired public env var inlines as `undefined` and 400s every call.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## The shapes you read (field cheat-sheet)
|
|
57
|
+
|
|
58
|
+
V2 service objects are **flat**. The exact paths the frontend reads, and the plausible-wrong sibling each is mistaken for:
|
|
59
|
+
|
|
60
|
+
```jsonc
|
|
61
|
+
// services.queryServices(...).find() → result.items[] (filter out s.hidden)
|
|
62
|
+
service = {
|
|
63
|
+
_id, // links · cart catalogItemId base (NOT .id → undefined)
|
|
64
|
+
name, description, tagLine,
|
|
65
|
+
type, // "APPOINTMENT" | "CLASS"
|
|
66
|
+
hidden, // skip hidden services
|
|
67
|
+
mainSlug: { name }, // the URL slug (NOT service.slug; fallback supportedSlugs[0].name)
|
|
68
|
+
schedule: { id, availabilityConstraints: { sessionDurations: [60] } }, // duration = sessionDurations[0] (minutes)
|
|
69
|
+
payment: { fixed: { price: { value, currency } } }, // value is a STRING; currency is the site's
|
|
70
|
+
media: { mainMedia: { image } }, // image is a "wix:image://…" string — resolve via media.getImageUrl()
|
|
71
|
+
category: { _id }, // the service's category
|
|
72
|
+
form: { _id }, // the booking form (fetch via @wix/forms)
|
|
73
|
+
bookingPolicy: { cancellationFeePolicy: { enabled } }, // drives the checkout-vs-place decision
|
|
74
|
+
staffMemberDetails: { staffMembers: [{ staffMemberId, name }] }, // STAFF_MEMBER_DETAILS conditional field
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// availabilityTimeSlots.listAvailabilityTimeSlots(...) → result.timeSlots[] (APPOINTMENT)
|
|
78
|
+
apptSlot = { localStartDate, localEndDate, scheduleId, location, availableResources } // fields at TOP level (NOT slot.startDate); availableResources may be []
|
|
79
|
+
// availableResources[] = { resources: [{ _id, name }] } // NESTED; each resource id is _id NOT id — this is what you read to label/group a slot by its staff/instructor
|
|
80
|
+
|
|
81
|
+
// eventTimeSlots.listEventTimeSlots(...) → result.timeSlots[] (CLASS)
|
|
82
|
+
classSlot = { localStartDate, localEndDate, eventInfo: { eventId } } // carries eventId; NO scheduleId
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**⚠️ CRITICAL: entity ids are `_id`, NOT `id`.** `service._id`, `booking._id`. `service.id` is `undefined` in SDK code. This includes the **resource objects nested under a slot** — `slot.availableResources[].resources[]._id` (NOT `.id`); reading `.id` there silently yields `undefined`, so grouping/labeling/filtering slots by staff or instructor breaks with no error (an image-led class site rendering blank instructor names/photos is the tell). **`staffMemberDetails.staffMembers[].staffMemberId`** is the exception — despite the name it **IS the resource GUID** (it matches the seed's staff `resourceId`), and it's the value you filter availability and book by.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## The booking features (build the ones the site needs)
|
|
90
|
+
|
|
91
|
+
Each section is a **self-contained feature** — implement only the ones the site uses; the only ordering is *within* the flow (pick a slot → collect the form → book). The minimal path is: list services → pick a slot → submit the form → book.
|
|
92
|
+
|
|
93
|
+
### Listing services (and the `_id` rule)
|
|
94
|
+
|
|
95
|
+
Always filter by `appId`, request `STAFF_MEMBER_DETAILS`, and call `.find()`:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// Catalog (list):
|
|
99
|
+
const { items } = await services
|
|
100
|
+
.queryServices({ conditionalFields: ['STAFF_MEMBER_DETAILS'] })
|
|
101
|
+
.eq('appId', BOOKING_APP_ID).limit(100).find(); // filter out items where s.hidden
|
|
102
|
+
|
|
103
|
+
// Single service by slug — the same .eq() builder chain:
|
|
104
|
+
const { items: [service] } = await services
|
|
105
|
+
.queryServices({ conditionalFields: ['STAFF_MEMBER_DETAILS'] })
|
|
106
|
+
.eq('mainSlug.name', slug).eq('appId', BOOKING_APP_ID).limit(1).find();
|
|
107
|
+
```
|
|
108
|
+
Doc: <https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md?apiView=SDK>
|
|
109
|
+
|
|
110
|
+
- **⚠️ `queryServices` is a fluent builder — its only argument is `QueryServicesOptions` (`conditionalFields`, etc.), NOT a `{ query: { filter, paging } }` object.** Apply the `appId` filter and paging as `.eq(...)` / `.limit(...)` chain calls, then `.find()`. The `{query:{filter,paging}}` form is the REST body shape; it does **not** type-check against the SDK (`'query' does not exist in type 'QueryServicesOptions'`) — if the SDK reference page shows an object-param form, trust the installed builder, not that doc example.
|
|
111
|
+
|
|
112
|
+
- **`appId` filter is required** — omitting it returns services from other apps / nothing useful.
|
|
113
|
+
- **Read V2 flat fields** (cheat-sheet): slug `mainSlug.name` (**not** `service.slug`), `name`/`description`/`tagLine`, duration `schedule.availabilityConstraints.sessionDurations[0]`, price `payment.fixed.price.value` (a **string**). A missing service usually means it wasn't seeded **with a category** (the visibility invariant), not a query bug.
|
|
114
|
+
|
|
115
|
+
### Filtering — location & category (both auto-skip)
|
|
116
|
+
|
|
117
|
+
Both filter the **catalog query** and re-render. **Show each filter only when there is more than one choice.**
|
|
118
|
+
|
|
119
|
+
- **Category:** `categoriesV2.queryCategories().find()` → `result.items[{ _id, name }]`. **Non-fatal** — wrap in try/catch and render without the bar if it fails. Show the bar only when `items.length > 1` (the seed creates at least one category, so a single-category site shows no bar). Filter: `filter['category.id'] = { $eq: categoryId }`; a service's category is `service.category._id`.
|
|
120
|
+
- **Location:** `services.queryLocations()` → `{ businessLocations, customLocations: { exists }, customerLocations: { exists } }`. Show the selector only when the total count > 1. A real business id → `filter['locations.business.id'] = { $hasSome: [id] }`; the synthetic `"custom"`/`"customer"` → `filter['locations.type'] = { $hasSome: ['CUSTOM'|'CUSTOMER'] }`. Carry the chosen location to the slots step so availability is scoped too (next section).
|
|
121
|
+
|
|
122
|
+
### Availability (the slots step) — appointment vs class
|
|
123
|
+
|
|
124
|
+
**⚠️ CRITICAL: APPOINTMENT and CLASS use different namespaces, and the slot fields are at the TOP level.**
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// APPOINTMENT — serviceId is a single GUID STRING:
|
|
128
|
+
const { timeSlots } = await availabilityTimeSlots.listAvailabilityTimeSlots({
|
|
129
|
+
serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
|
|
130
|
+
// staff (optional): resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [staffMemberId] }],
|
|
131
|
+
// includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
|
|
132
|
+
// one business location: locations: [{ _id: locationId, locationType: 'BUSINESS' }],
|
|
133
|
+
}); // slots: result.timeSlots[] — localStartDate/localEndDate/scheduleId at TOP level
|
|
134
|
+
|
|
135
|
+
// CLASS — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, NO scheduleId:
|
|
136
|
+
const { timeSlots } = await eventTimeSlots.listEventTimeSlots({
|
|
137
|
+
serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false,
|
|
138
|
+
// staff/location (optional): eventFilter: { 'resources.id': { $hasSome: [staffMemberId] }, 'location.id': [locationId] },
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
Docs: <https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md?apiView=SDK> · <https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-event-time-slots.md?apiView=SDK>
|
|
142
|
+
|
|
143
|
+
- **`fromLocalDate`/`toLocalDate` are LOCAL strings `YYYY-MM-DDThh:mm:ss` (no `Z`)** plus a separate `timeZone`. Passing UTC ISO strings with `Z` returns the wrong window.
|
|
144
|
+
- **Slot fields are top-level** — read `timeSlot.localStartDate` / `.localEndDate` / `.scheduleId`, NOT `timeSlot.slot.startDate`. CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`.
|
|
145
|
+
- **⚠️ Scope availability to exactly ONE business location on a multi-location service.** Unscoped, `listAvailabilityTimeSlots` returns **one slot per location** per time → every time appears N× (duplicated rows). Build the location list from `queryLocations()` intersected with the service's location ids (a `service.locations` id alone can be one the availability engine doesn't recognize → 0 slots). Staff does **not** multiply rows — one slot carries many `availableResources`.
|
|
146
|
+
- **Staff picker auto-skips ≤1 staff** (`service.staffMemberDetails.staffMembers`). Filter by `staffMemberId` (the resource GUID). When a slot is picked with a specific staff, record `resource = { _id: staffMemberId, name }`; with "any staff", leave it unset (the booking emits the ANY_RESOURCE fallback below).
|
|
147
|
+
- **Group slots by day** — don't render a flat time-only grid (the visitor can't tell which day a slot is on). Week / month / N-day is your choice.
|
|
148
|
+
|
|
149
|
+
### The booking form — schema-driven (`@wix/forms`)
|
|
150
|
+
|
|
151
|
+
The booking form is a `@wix/forms` form attached to the service at `service.form._id`. Render it **schema-driven** — never hardcode field names.
|
|
152
|
+
|
|
153
|
+
**⚠️ CRITICAL: use `getFormSummary`, NOT `getForm`.** `getForm` returns the full nested schema (`form.formFields[].inputOptions.stringOptions.componentType…`), and hand-parsing that deep shape is the #1 source of a **silently empty form** — when your guess at the nested path is wrong, the parse yields zero fields and the form renders no inputs (→ empty `formSubmission` → `createBooking` 400 `first_name must have required property`) or hangs on a "loading" state. Use the **flat** summary instead:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
const { formSummary } = await forms.getFormSummary(service.form._id);
|
|
157
|
+
// formSummary.fields[] = { target, label, type, options?, deleted, _id } ← FLAT, no nesting
|
|
158
|
+
const fields = (formSummary?.fields ?? [])
|
|
159
|
+
.filter(f => !f.deleted)
|
|
160
|
+
.filter(f => f.type && ['STRING', 'EMAIL', 'PHONE', 'NUMBER', 'URL'].includes(f.type)); // simple text-like only (`f.type` is optional on the summary field — guard it or strict build errors)
|
|
161
|
+
```
|
|
162
|
+
Doc: <https://dev.wix.com/docs/api-reference/crm/forms/form-schemas/get-form-summary.md?apiView=SDK> · bookings↔forms: <https://dev.wix.com/docs/api-reference/business-solutions/bookings/wix-forms-integration.md?apiView=SDK>
|
|
163
|
+
|
|
164
|
+
- **`target` is the submission key** (top-level on each field — no `inputOptions` digging). `type` is a clean enum: render `EMAIL`→`type=email`, `PHONE`→`type=tel`, `NUMBER`→`type=number`, else text; a field with `options[]`→a `<select>`.
|
|
165
|
+
- Each input **writes straight into one values object keyed by `target`** (`{ [target]: value }`). **That object IS the `formSubmission`** — pass it directly to `createBooking`. Do **not** rebuild a second object by looping the schema at submit time.
|
|
166
|
+
|
|
167
|
+
**⚠️ CRITICAL: render the contact-basics fallback whenever the parsed list is EMPTY — not only on a thrown error.** A successful `getFormSummary` that yields zero usable fields (all filtered out, or an unexpected shape) must still produce a working form. Default to `first_name` / `last_name` / `email` (the booking enforces exactly these) so the form always renders and submits:
|
|
168
|
+
```js
|
|
169
|
+
const formFields = fields.length ? fields : [
|
|
170
|
+
{ target: 'first_name', label: 'First Name', type: 'STRING' },
|
|
171
|
+
{ target: 'last_name', label: 'Last Name', type: 'STRING' },
|
|
172
|
+
{ target: 'email', label: 'Email', type: 'EMAIL' },
|
|
173
|
+
];
|
|
174
|
+
```
|
|
175
|
+
**Never gate the form's render on a loading flag that can't resolve** (e.g. showing "Loading form…" while `formFields.length === 0`) — with the fallback above the list is never empty, so render directly.
|
|
176
|
+
|
|
177
|
+
**⚠️ Skip complex, object-valued field types** — `MULTILINE_ADDRESS`, `OBJECT`, `ARRAY`, `WIX_FILE`, `SIGNATURE`, `PAYMENT`, `DATE`/`TIME` (the filter above already keeps only the simple text-like types). Rendering one as a text input sends a string where the API wants an object and `createBooking` rejects it with **"must be object"**. Only the contact basics are enforced, so dropping optional complex fields is safe.
|
|
178
|
+
|
|
179
|
+
### Creating the booking — the createBooking → ecom Cart V2 sequence
|
|
180
|
+
|
|
181
|
+
This is one feature with a fixed sequence. **Use it as-is — the payload shapes are exact and easy to get subtly wrong.** APPOINTMENT and CLASS share the same sequence; they differ only in the slot fields.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
createBooking(...) // booking lands CREATED — the cart holds the seat (no confirmBooking)
|
|
185
|
+
createCart(...) // one catalogItem per bookingId, appId = BOOKING_APP_ID, channelType WEB
|
|
186
|
+
calculateCart(cartId) // → { cart, summary }; totals on summary.priceSummary.total.amount
|
|
187
|
+
isCheckoutRequired ? redirect to hosted checkout (paid) : placeOrder(cartId) (free / pay-in-person)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**1 · createBooking.** Doc: <https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md?apiView=SDK>
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
const created = await bookings.createBooking({
|
|
194
|
+
selectedPaymentOption, // derive from service.payment.options — see below
|
|
195
|
+
totalParticipants: 1,
|
|
196
|
+
bookedEntity: {
|
|
197
|
+
slot: {
|
|
198
|
+
serviceId,
|
|
199
|
+
scheduleId: slot.scheduleId ?? undefined, // APPOINTMENT only — the SELECTED SLOT's own scheduleId, NOT service.schedule.id (the availability slot types it `string | null`; `?? undefined` to satisfy the `string | undefined` param under strict)
|
|
200
|
+
eventId: slot.eventInfo?.eventId, // CLASS only (Wix derives startDate/endDate/resource/location from it)
|
|
201
|
+
startDate: slot.localStartDate, // local "YYYY-MM-DDThh:mm:ss"
|
|
202
|
+
endDate: slot.localEndDate,
|
|
203
|
+
timezone,
|
|
204
|
+
// specific staff: resource: { _id: staffMemberId, name }
|
|
205
|
+
// any staff: resourceSelections: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, selectionMethod: 'ANY_RESOURCE' }]
|
|
206
|
+
location: { locationType: 'OWNER_BUSINESS' }, // map BUSINESS→OWNER_BUSINESS, CUSTOMER→CUSTOM
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}, { formSubmission }); // the form values keyed by `target`
|
|
210
|
+
const bookingId = created.booking._id;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- **⚠️ `scheduleId` MUST come from the SELECTED SLOT (`slot.scheduleId`), not from the service.** The APPOINTMENT slot carries its own top-level `scheduleId` (cheat-sheet) — carry it from the slots step into the booking. The service object *also* has a `schedule.id` (you read it for duration), but it is a **different value**: sending `service.schedule.id` (or an empty `scheduleId` prop) makes `createBooking` 400 with `VALIDATION_FAILURE: "…startDate, endDate, location.locationType, resource.id, and scheduleId must be set"`. (CLASS sends `slot.eventInfo.eventId` instead — no `scheduleId`.)
|
|
214
|
+
- **⚠️ Pass `formSubmission`, NOT `contactDetails`.** `contactDetails` is what Wix derives back onto the response; sending it instead of `formSubmission` drops the visitor's input.
|
|
215
|
+
- **⚠️ `formSubmission` is the object your inputs wrote to (values keyed by `target`) — send it DIRECTLY; do NOT rebuild it by iterating the parsed form-field schema.** If you re-derive the submission by looping over the schema array and your render falls back to a hardcoded field list when the schema parse comes back empty, the rebuild loop reads the empty schema and sends an **empty body** → `createBooking` 400s with `INVALID_ARGUMENT: "first_name must have required property 'first_name'…"` even though the form looked filled. Keep the typed-values object (`{ [target]: value }`) as the single source and pass that.
|
|
216
|
+
- **⚠️ ANY_RESOURCE fallback (staff).** With no specific staff chosen, send `resourceSelections: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, selectionMethod: 'ANY_RESOURCE' }]` and Wix auto-assigns a bookable resource — appointment slots can return `availableResources: []` yet book fine this way. A specific choice sends `resource: { _id, name }` instead.
|
|
217
|
+
- **⚠️ Derive `selectedPaymentOption` from the service — do NOT hardcode `"ONLINE"`.** `online && !inPerson → "ONLINE"`; `!online && inPerson → "OFFLINE"`; else `"ONLINE"`. A **free / pay-in-person** service booked `ONLINE` makes the cart reject it with **`INSUFFICIENT_INVENTORY`** (`available_quantity: 0`).
|
|
218
|
+
- **`locationType` on the booking slot is `OWNER_BUSINESS`** (the booking endpoint's enum), NOT `BUSINESS` (which is the *services* endpoint's enum). Map the slot's `BUSINESS`→`OWNER_BUSINESS`, `CUSTOMER`→`CUSTOM`.
|
|
219
|
+
- **Wrap in try/catch** — `createBooking` can reject (slot taken between fetch and submit, strict phone validation). Surface a friendly message; don't crash.
|
|
220
|
+
|
|
221
|
+
**2 · Cart holds the seat → checkout or place.**
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const cart = await createCart({
|
|
225
|
+
catalogItems: [{ quantity: 1, catalogReference: { catalogItemId: bookingId, appId: BOOKING_APP_ID } }],
|
|
226
|
+
cart: { source: { channelType: 'WEB' } },
|
|
227
|
+
});
|
|
228
|
+
if (!cart._id) throw new Error('cart creation failed'); // Cart V2 types `_id` as `string | undefined`; narrow once so the strict build accepts it downstream
|
|
229
|
+
const cartId = cart._id;
|
|
230
|
+
const { cart: calc, summary } = await calculateCart(cartId); // totals are NOT stored on the Cart V2 entity
|
|
231
|
+
|
|
232
|
+
// checkout required? cancellation-fee policy → yes; total 0 → no; FULL_PAYMENT_OFFLINE → no; else yes
|
|
233
|
+
if (checkoutRequired) {
|
|
234
|
+
const { redirectSession } = await redirects.createRedirectSession({
|
|
235
|
+
ecomCheckout: { checkoutId: cartId }, // the cartId IS the checkoutId here
|
|
236
|
+
callbacks: { postFlowUrl: `${origin}/booking-confirmation` },
|
|
237
|
+
});
|
|
238
|
+
window.location.href = redirectSession.fullUrl; // Wix-hosted checkout
|
|
239
|
+
} else {
|
|
240
|
+
const order = await placeOrder(cartId); // free / pay-in-person → confirmation
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **⚠️ No `confirmBooking`.** The classic server-side confirm step is not used — the **ecom Cart V2** holds the seat: `placeOrder` (free/offline) or the hosted checkout (paid) drives confirmation, so a client-only site completes the whole flow with no server elevation.
|
|
245
|
+
- **⚠️ CRITICAL: `origin` in `postFlowUrl` MUST be the `https://` published host — from `window.location.origin`, NEVER `new URL(request.url).origin`.** The Headless redirect allowlist registers the `https://` host and treats `http://<same host>` as a different, unlisted origin; a `http://` `postFlowUrl` makes the return ("Continue Browsing") **403** with *"… isn't listed as an allowed redirect domain."* Server-derived `new URL(request.url).origin` is `http://` behind Wix's proxy → pass `window.location.origin` from the client, or force `https`. Doc: <https://dev.wix.com/docs/go-headless/getting-started/setup/manage-urls/add-allowed-redirect-domains>.
|
|
246
|
+
- **⚠️ CRITICAL: the confirmation page must reflect REAL status — do NOT hardcode `?status=success` in `postFlowUrl`, and do NOT default the page to success.** The buyer lands on `postFlowUrl` whether they completed payment **or** clicked "Continue Browsing" to abandon the hosted checkout. A confirmation page that reads `status ?? 'success'` (or trusts a `?status=success` you baked into the URL) renders "confirmed" for an **unpaid, abandoned** booking. Drive the confirmation from the actual order/booking — look up the booking (it's `CREATED` after `createBooking`; payment is a separate state) or the order — and don't present an unconfirmed booking as confirmed. For the free/offline branch, `placeOrder` having returned an `orderId` is a real success signal; for the paid branch, returning from the redirect is **not**.
|
|
247
|
+
- **⚠️ Read the booking back as the anonymous visitor — `getBooking` is a manage-scope read that `403`s the visitor (same axis as the events `ticketDefinitions` trap; do NOT `auth.elevate`).** The visitor-public path is a two-step token handoff, both exports on `@wix/bookings` — use these exact calls, don't hunt the installed `.d.ts` for them:
|
|
248
|
+
1. **At booking time** (right after `createBooking` returns `booking._id`) mint a one-shot read token: `const { token } = await bookings.getAnonymousActionToken(bookingId)` — **`bookingId` is a POSITIONAL string arg, not `{ bookingId }`** (the object form fails `tsc`). Append the token to your `postFlowUrl` (e.g. `?bookingId=…&token=…`).
|
|
249
|
+
2. **On the confirmation page** read it back: `const { booking } = await bookings.bookingsGetBookingAnonymously(token)` — the response is **wrapped** (`{ booking }`). Drive the UI off **`booking.status`** (`CREATED` = seat held / payment pending; `CONFIRMED`/`PENDING` = done) and the time off **`booking.bookedEntity.slot.startDate`**.
|
|
250
|
+
- The cart's `catalogReference.catalogItemId` is the **`booking._id`** and `appId` is the **Bookings app id** (a booking id is automatically valid as an ecom catalog item).
|
|
251
|
+
|
|
252
|
+
### Rendering & mounting
|
|
253
|
+
|
|
254
|
+
- **Price**: `service.payment.fixed.price.value` (string) + `.currency` (the site's stored currency — format from it, don't assume USD).
|
|
255
|
+
- **Image**: `service.media.mainMedia.image` is a **string**, not an object (the installed type is `image?: string`, so `.image.url` fails `tsc`) — but its value is a Wix media URI like `wix:image://v1/…`, **not** an absolute URL (`.startsWith("https://")` is `false`). Putting it straight into `<img src>` ships broken images. Resolve it first: `import { media } from "@wix/sdk"`, then `media.getImageUrl(service.media.mainMedia.image).url` → `https://static.wixstatic.com/media/…`. Guard for services with no image.
|
|
256
|
+
- **Mount slots + form + book in a `client:only="react"` island** (Astro) — availability is timezone/session-specific. SSR only the read pages (catalog/detail) for SEO.
|
|
257
|
+
|
|
258
|
+
### SEO on item pages (Astro, Wix-managed)
|
|
259
|
+
|
|
260
|
+
A **service detail** page is a Wix **item page**: its `<title>`/description/OG/canonical come from what the owner sets in the dashboard. On the Astro (Wix-managed) frontend, wire it per the canonical guide — **[Add SEO Support to Item Pages](https://dev.wix.com/docs/go-headless/wix-managed-headless/seo/add-seo-support-to-item-pages.md)** — which covers the three steps: export `wixMetadata` (registers the route → sitemap + dashboard SEO editor), call `loadSEOTagsServiceConfig(...)`, and render `<SEO.Tags>` (from `@wix/seo`; deps + `@wix/essentials ≥ 1.0.10` are in the guide's "Before you begin").
|
|
261
|
+
|
|
262
|
+
For a service page use:
|
|
263
|
+
- **`wixMetadata`** from `WIX_APPS.bookings.servicePageMetadata` — referenced **directly** in the export (module scope). Route param `slug` → `identifiers.slug`.
|
|
264
|
+
- **`itemType`**: `seoTags.ItemType.BOOKINGS_SERVICE`.
|
|
265
|
+
|
|
266
|
+
Run `loadSEOTagsServiceConfig` in the same `Promise.all` as the service read, with `.catch(() => null)`, so a SEO hiccup falls back to the layout's default title instead of failing the page. Optional: render a `Service` schema.org JSON-LD `<script>` from the fetched service (see the guide's structured-data step).
|
|
267
|
+
|
|
268
|
+
### Out of scope
|
|
269
|
+
Waitlist, on-site manage/cancel, payment/deposit breakdown, multi-service packages, and COURSE bookings. Waitlist and manage/cancel have **no headless source of truth** — don't invent them on the anonymous-token layer; post-booking self-service is the Wix-hosted flow / member area.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Conclusion
|
|
274
|
+
A correct Services V2 booking frontend:
|
|
275
|
+
- imports **`services` / `availabilityTimeSlots` / `eventTimeSlots` / `bookings`** from `@wix/bookings`, plus **Cart V2** (`@wix/auto_sdk_ecom_cart-v-2`), `@wix/redirects`, and `@wix/forms` — and never the nonexistent `availability` namespace or `confirmBooking`;
|
|
276
|
+
- uses **`service._id`** (never `service.id`) and reads the **flat V2 fields** (`mainSlug.name`, `payment.fixed.price.value`, `schedule.availabilityConstraints.sessionDurations[0]`);
|
|
277
|
+
- fetches availability per type (**`availabilityTimeSlots`** for APPOINTMENT, **`eventTimeSlots`** for CLASS) with **local date strings**, reads slot fields at the **top level**, and scopes to one location to avoid duplicate rows;
|
|
278
|
+
- renders the booking form **schema-driven via `getFormSummary`** (the flat `formSummary.fields[].target`, never `getForm`/`formFields`), **falls back to `first_name`/`last_name`/`email` whenever the parsed list is empty** (so the form never renders blank or hangs), keys values by `target`, skips complex field types, and passes that object as **`formSubmission`** (not `contactDetails`);
|
|
279
|
+
- runs **createBooking → createCart → calculateCart → checkout-or-place**, deriving `selectedPaymentOption` from the service (free/offline → `OFFLINE`, else `ONLINE` — the wrong choice rejects the cart with `INSUFFICIENT_INVENTORY`) and using the **ANY_RESOURCE** fallback when no staff is chosen.
|