@wix/agent-skills 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
  3. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
  4. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
  5. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
  6. package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
  7. package/skills/wix-design-system/SKILL.md +1 -1
  8. package/skills/wix-headless/references/BUILD-astro.md +5 -7
  9. package/skills/wix-headless/references/SETUP.md +1 -1
  10. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  11. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
  12. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
  13. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
  14. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  15. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
  16. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
  17. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  18. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
  19. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
  20. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
  21. package/skills/wix-headless/references/bookings/FLOW.md +185 -0
  22. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
  23. package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
  24. package/skills/wix-headless/references/verticals/bookings.md +6 -8
  25. package/skills/wix-manage/SKILL.md +12 -0
  26. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  27. package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
  28. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  29. package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
  30. package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
  31. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  32. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  33. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  34. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  35. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  36. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  37. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -5,26 +5,77 @@ import Layout from "../../layouts/Layout.astro";
5
5
  import SeoTags from "../../components/SeoTags.astro";
6
6
  import ServiceBookingFlow from "../../components/ServiceBookingFlow";
7
7
  import { services } from "@wix/bookings";
8
+ import { forms } from "@wix/forms";
8
9
  import { auth } from "@wix/essentials";
9
10
 
11
+ const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
10
12
  const { slug } = Astro.params;
11
13
 
12
14
  let service: any = null;
13
15
  try {
14
- // Ambient SSR via @wix/essentials; query BUILDER (object form returns 0).
15
- // Fetch the small catalog and match the slug in JS (robust without a
16
- // nested-field `mainSlug.name` builder filter).
17
- const result = await auth.elevate(services.queryServices)().limit(100).find();
18
- service =
19
- (result.items ?? []).find(
20
- (s: any) => (s.mainSlug?.name ?? s.supportedSlugs?.[0]?.name) === slug,
21
- ) ?? null;
16
+ // Ambient SSR via @wix/essentials. Single service by slug: the .eq() builder
17
+ // chain on mainSlug.name + appId (the form the @wix/bookings SDK expects).
18
+ const result = await auth
19
+ .elevate(services.queryServices)({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
20
+ .eq("mainSlug.name", slug)
21
+ .eq("appId", BOOKING_APP_ID)
22
+ .limit(1)
23
+ .find();
24
+ service = result.items?.[0] ?? null;
22
25
  } catch (err) {
23
26
  console.error(`[services/${slug}] detail query failed:`, err);
24
27
  }
25
28
 
26
29
  if (!service) return Astro.redirect("/services", 302);
27
30
 
31
+ // Booking-form SCHEMA — the form belongs to the service (service.form._id) in the
32
+ // bookings namespace. Read it server-side and pass the field list to the island.
33
+ // Same renderer the forms vertical uses (references/astro/forms/CONTACT_FORM.md):
34
+ // keep the field's `target` — that is the key createBooking expects in formSubmission.
35
+ let formFields: any[] = [];
36
+ try {
37
+ const formId = service.form?._id;
38
+ if (formId) {
39
+ const res: any = await auth.elevate(forms.getForm)(formId);
40
+ const form = res?.form ?? res;
41
+ // Only render field types we can submit as a simple value. Skip complex,
42
+ // object-valued fields (e.g. a multi-line ADDRESS) — they carry no string
43
+ // `componentType`, and sending a string for them fails createBooking with
44
+ // "must be object". The booking enforces only the contact basics
45
+ // (first_name/last_name/email), so skipping optional complex fields is safe.
46
+ const RENDERABLE = ["TEXT_INPUT", "PHONE_INPUT", "DROPDOWN"];
47
+ formFields = (form?.formFields ?? [])
48
+ .filter(
49
+ (f: any) =>
50
+ f.fieldType === "INPUT" &&
51
+ !f.hidden &&
52
+ RENDERABLE.includes(f.inputOptions?.stringOptions?.componentType),
53
+ )
54
+ .map((f: any) => {
55
+ const opts = f.inputOptions;
56
+ const str = opts?.stringOptions;
57
+ const label =
58
+ str?.textInputOptions?.label ??
59
+ str?.dropdownOptions?.label ??
60
+ str?.phoneInputOptions?.label ??
61
+ "";
62
+ return {
63
+ label,
64
+ target: opts?.target ?? "",
65
+ required: opts?.required ?? false,
66
+ componentType: str?.componentType ?? "TEXT_INPUT",
67
+ identifier: f.identifier ?? "",
68
+ options: str?.dropdownOptions?.options?.map((o: any) => ({
69
+ value: o.value ?? o.label ?? "",
70
+ label: o.label ?? o.value ?? "",
71
+ })),
72
+ };
73
+ });
74
+ }
75
+ } catch (err) {
76
+ console.error(`[services/${slug}] booking form schema failed:`, err);
77
+ }
78
+
28
79
  const formatPrice = (p: any): string | undefined => {
29
80
  if (!p || p.rateType === "NO_FEE") return undefined;
30
81
  if (p.rateType === "FIXED" && p.fixed?.price) {
@@ -71,9 +122,10 @@ const seoTags = service.seoData?.tags ?? [];
71
122
  <section class="border-t border-rule pt-2xl">
72
123
  <h2 class="font-display text-3xl leading-tight mb-xl">Book this service</h2>
73
124
  <ServiceBookingFlow
74
- serviceId={service._id}
125
+ service={service}
75
126
  serviceName={serviceName}
76
127
  serviceType={service.type ?? "APPOINTMENT"}
128
+ fields={formFields}
77
129
  client:only="react"
78
130
  />
79
131
  </section>
@@ -7,13 +7,21 @@ import ServiceCard from "../../components/ServiceCard.astro";
7
7
  import { services } from "@wix/bookings";
8
8
  import { auth } from "@wix/essentials";
9
9
 
10
+ // Wix Bookings app id — the base filter must scope queryServices to it.
11
+ const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
12
+
10
13
  let serviceList: any[] = [];
11
14
  try {
12
15
  // Ambient @wix/astro SSR client via @wix/essentials (same pattern as CMS pages).
13
16
  // Do NOT build createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) }) —
14
17
  // that env var is client-only and undefined at server render → 500.
15
- // queryServices uses the query BUILDER; the object form returns 0 items.
16
- const result = await auth.elevate(services.queryServices)().limit(100).find();
18
+ // Pass the request object (filter must include appId) + conditionalFields, then .find().
19
+ const result = await auth
20
+ .elevate(services.queryServices)({
21
+ query: { filter: { appId: BOOKING_APP_ID }, paging: { limit: 100 } },
22
+ conditionalFields: ["STAFF_MEMBER_DETAILS"],
23
+ })
24
+ .find();
17
25
  serviceList = (result.items ?? []).filter((s: any) => !s.hidden);
18
26
  } catch (err) {
19
27
  console.error("[services] listing query failed:", err);
@@ -0,0 +1,185 @@
1
+ # Bookings — the booking flow (framework-agnostic logic)
2
+
3
+ The booking logic — the step model, the shared state, and the exact `@wix` SDK
4
+ sequence — is the **same on every frontend** (astro, react/vite, vue, plain HTML).
5
+ This file is that shared logic. The per-framework wiring layers on top:
6
+
7
+ - **astro** (`frontendBuild: wix`): `../astro/bookings/SERVICES_PAGES.md` (SSR read pages) + `../astro/bookings/COMPONENTS.md` (the client islands).
8
+ - **own / static** (`frontendBuild: own`/`none`): `../custom/bookings/WIRING.md`.
9
+
10
+ **Code examples are React** (`../astro/templates/bookings/*.tsx` + `bookingDriver.ts`).
11
+ React is directly usable on astro islands and on react/vite. **On any other
12
+ framework, translate the examples** — the SDK calls and payloads are identical;
13
+ only the UI idiom (`useState`/`useEffect` → a store / `onMounted` / signals) and
14
+ the client acquisition differ (see "Client identity" below).
15
+
16
+ The flow is **appointment booking**. CLASS sign-up reuses the same sequence with
17
+ an event-slot branch (noted inline) and is a fast-follow, not v1. **Location and
18
+ staff selection are likewise a fast-follow (a separate PR); add-ons are out of
19
+ scope.** v1 threads a single service selection — the shared state below holds no
20
+ location / staff / add-on choice yet.
21
+
22
+ ---
23
+
24
+ ## 1. The step model
25
+
26
+ A single customer journey, five steps, holding one shared selection:
27
+
28
+ ```
29
+ 1. Catalog list bookable services → pick a service
30
+ 2. Slots week calendar of availability for that service → pick a time slot
31
+ 3. Details the service's booking form (collect contact) → submit
32
+ 4. Book run the SDK sequence (§3)
33
+ 5a. paid → redirect to the Wix-hosted checkout → returns to the confirmation page
34
+ 5b. free/offline → place the order → confirmation page
35
+ ```
36
+
37
+ **The catalog is the entry point but optional** — a visitor can land directly on a
38
+ service page and start at the Slots step. Every step rehydrates from the URL (§2),
39
+ so deep links work.
40
+
41
+ Mapping the steps onto pages/routes is the **target framework's choice** — translate
42
+ the step model to whatever router the framework uses. The skill recommends the routes
43
+ `/services`, `/services/[slug]`, `/booking-confirmation` (advertised in
44
+ `../verticals/bookings.md`). On astro specifically, SSR the catalog + detail pages
45
+ for SEO and run slots + form + book in a `client:only` island.
46
+
47
+ ## 2. Shared booking state
48
+
49
+ Steps 1–4 build up one selection (this is exactly `BookParams` + `SelectedSlot`
50
+ in `../astro/templates/bookings/bookingDriver.ts`):
51
+
52
+ - **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`).
53
+ - **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType? }`.
54
+ - **`formSubmission`** — the booking-form values, **keyed by each field's `target`** (§4).
55
+
56
+ Hold it however the framework prefers (React state lifted to a coordinator, a
57
+ store, route loaders + a provider, query-params). **Two rules that bite:**
58
+
59
+ 1. **Persist the selection before navigating.** When a step lives on its own
60
+ route, write the selection into the shared state (or the URL) *before* moving
61
+ on, and let the next step **rehydrate from the slug/URL** on direct load /
62
+ refresh. A persistent provider that does not re-init on in-app navigation will
63
+ otherwise show an empty next step.
64
+ 2. **The slug is the carry-across key.** `serviceSlug` (the service's
65
+ `mainSlug.name`) identifies the service across the slots + details steps;
66
+ resolve the full service from it when state is lost.
67
+
68
+ ## 3. The booking SDK sequence — `bookingDriver.ts`
69
+
70
+ The whole booking — `createBooking → createCart → calculateCart → checkout-or-place`
71
+ — is in **`../astro/templates/bookings/bookingDriver.ts`**, in plain `@wix` SDK
72
+ calls (no React, no UI). **Use it as-is** (it is framework-agnostic); the booking
73
+ step calls `book(params)` then branches on the result. Do not re-author the
74
+ sequence — the payload shapes are exact and easy to get subtly wrong.
75
+
76
+ ```
77
+ book({ service, slot, formSubmission, timezone }) →
78
+ createBooking(...) // booking lands CREATED — the cart holds the seat
79
+ createCart(...) // one catalog item per bookingId, appId = BOOKING_APP_ID, channel WEB
80
+ calculateCart(cartId) // → { cart, summary }; totals on summary.priceSummary.total.amount
81
+ isCheckoutRequired(...) ?
82
+ → { CheckoutRequired, cartId } // paid → navigateToCheckout(cartId, postFlowUrl) → Wix-hosted checkout
83
+ : placeOrder(cartId) → { CheckoutSkipped, orderId } // free / pay-in-person → confirmation
84
+ ```
85
+
86
+ Key facts the driver encodes (do not deviate):
87
+ - **No `confirmBooking`** — `confirmBooking` is the classic Bookings server-side
88
+ confirm step (it moves a booking from `CREATED` to `CONFIRMED`). Here the **ecom
89
+ cart holds the seat** instead — `placeOrder` (free/offline) or the hosted checkout
90
+ (paid) drives confirmation — so a client-only site completes the whole flow with no
91
+ server elevation.
92
+ - **ANY_RESOURCE fallback (staff)** — v1 has **no staff picker**, so `createBooking`
93
+ always sends `resourceSelections:[{ resourceTypeId:"1cd44cf8-…", selectionMethod:"ANY_RESOURCE" }]`
94
+ and Wix auto-assigns a bookable staff resource (appointment slots return
95
+ `availableResources:[]` yet book fine this way). (Explicit staff selection is a
96
+ fast-follow.)
97
+ - **Checkout decision** — `isCheckoutRequired`: if the service's booking policy charges
98
+ a **cancellation fee** (`service.bookingPolicy.cancellationFeePolicy.enabled`) →
99
+ checkout (a card must be on file); else total 0 → place; else `FULL_PAYMENT_OFFLINE`
100
+ → place; else checkout.
101
+ - **Redirect shape** — paid bookings hand the cart to the Wix-hosted **ecom**
102
+ checkout: `createRedirectSession({ ecomCheckout: { checkoutId: cartId }, callbacks:{ postFlowUrl } })`.
103
+
104
+ ### Browse + availability (the read calls the steps make)
105
+
106
+ Mirror what the Bookings SDK does (`services.queryServices` is the raw SDK, not a component):
107
+
108
+ ```js
109
+ // Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find():
110
+ services.queryServices({
111
+ query: { filter: { appId: BOOKING_APP_ID }, paging: { limit: 100 } },
112
+ conditionalFields: ["STAFF_MEMBER_DETAILS"],
113
+ }).find(); // → result.items ; filter out s.hidden
114
+
115
+ // Single service by slug — the .eq() builder chain:
116
+ services.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
117
+ .eq("mainSlug.name", slug).eq("appId", BOOKING_APP_ID).limit(1).find();
118
+
119
+ // Availability (APPOINTMENT) — serviceId is a single GUID STRING:
120
+ availabilityTimeSlots.listAvailabilityTimeSlots({
121
+ serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
122
+ }); // slots: result.timeSlots[] (localStartDate/localEndDate/scheduleId at top level)
123
+
124
+ // Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId:
125
+ eventTimeSlots.listEventTimeSlots({ serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false });
126
+ ```
127
+
128
+ `fromLocalDate`/`toLocalDate` are **local** date strings `YYYY-MM-DDThh:mm:ss`
129
+ (no `Z`) with an explicit `timeZone`.
130
+
131
+ ## 4. The booking form — schema-driven (`@wix/forms`)
132
+
133
+ The booking form is a **`@wix/forms` form** attached to the service
134
+ (`service.form._id`, namespace `wix.bookings.v2.bookings`). Render it
135
+ **schema-driven** — read the field list and render inputs by field type — the
136
+ **same renderer the forms vertical uses** (`../astro/forms/CONTACT_FORM.md`):
137
+
138
+ 1. Fetch the form by `service.form._id` (`@wix/forms` `getForm`; server-side/elevated
139
+ where the framework allows). Read `form.formFields`.
140
+ 2. Keep `fieldType === "INPUT" && !hidden` **and** a recognized string
141
+ `componentType` (`TEXT_INPUT` / `PHONE_INPUT` / `DROPDOWN`). For each, take
142
+ `inputOptions.target`, `inputOptions.required`, `inputOptions.stringOptions.componentType`,
143
+ and the label from `inputOptions.stringOptions.{textInputOptions|dropdownOptions|phoneInputOptions}.label`.
144
+ **Skip complex, object-valued fields** — e.g. a multi-line `ADDRESS` — they carry
145
+ no string `componentType`; rendering them as a text input sends a string and
146
+ `createBooking` rejects it with **"must be object"**. The booking enforces only
147
+ the contact basics (`first_name`/`last_name`/`email`), so omitting an optional
148
+ complex field is safe (do **not** default unknown field types to a text input).
149
+ 3. Render generic inputs by `componentType`: `TEXT_INPUT` → text input,
150
+ `DROPDOWN` → select, `PHONE_INPUT` → `type=tel`; treat `identifier === "TEXT_AREA"`
151
+ (or `target` containing `message`) as a textarea (its `componentType` is still `TEXT_INPUT`).
152
+ 4. Collect the values into an object **keyed by each field's `target`** — that is
153
+ `formSubmission`. Pass it to `book()`. Only include fields the visitor filled;
154
+ never send a value for a field you didn't render.
155
+
156
+ **Do not** submit `contactDetails`, and **do not** hardcode field names — key by
157
+ `target`. The default booking form's targets are snake_case `first_name` /
158
+ `last_name` / `email` / `phone`. Reference example: `../astro/templates/bookings/BookingForm.tsx`.
159
+
160
+ ## 5. The day-calendar (slots step)
161
+
162
+ Show a **week calendar**, not a flat list of every slot: a 7-day strip with
163
+ week navigation → the picked day's times. A flat grid with time-only labels
164
+ leaves the visitor unable to tell which day a slot is on. Fetch availability for
165
+ the visible window (`fromLocalDate`/`toLocalDate` = the week bounds), group slots
166
+ by calendar day, and offer a **"check next availability"** action that probes
167
+ forward when a week is empty. Reference example:
168
+ `../astro/templates/bookings/AvailabilityCalendar.tsx`.
169
+
170
+ ## 6. Client identity (per framework)
171
+
172
+ The SDK calls are identical everywhere; only how you get the client differs:
173
+ - **astro** — SSR read pages use the ambient `@wix/essentials` client
174
+ (`auth.elevate(services.queryServices)(...)`); browser islands call the `@wix`
175
+ modules **ambiently** too (the `@wix/astro` visitor client, like the ecom
176
+ `CartView` island) — no `createClient`/`OAuthStrategy`.
177
+ - **own / static** — acquire a visitor client once:
178
+ `createClient({ modules, auth: OAuthStrategy({ clientId: appId }) })` and call
179
+ the same functions on it. CDN imports for `none`, bundled for `own`.
180
+
181
+ ## 7. Out of v1
182
+
183
+ Waitlist, on-site manage/cancel, location selector, staff filter, payment
184
+ breakdown, and CLASS sign-up are out of v1. Show bookable slots only;
185
+ post-booking self-service is handled by the Wix-hosted flow / member area.
@@ -1,106 +1,90 @@
1
1
  ---
2
2
  name: bookings-implementer
3
- description: "Implements Wix Bookings vertical — services catalog, service detail with availability calendar and booking flow, and booking confirmation. Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
3
+ description: "Implements the Wix Bookings vertical — services catalog, a service detail page with a week-calendar availability picker, a schema-driven booking form, and confirmation. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
4
4
  ---
5
5
 
6
6
  # Bookings Implementer
7
7
 
8
- Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups, site.json read pattern, return contract, style conventions, and common failure modes.
8
+ Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups, the `.wix/seeded.json` read pattern, the return contract, style conventions, and common failure modes.
9
+
10
+ ## The logic lives in one place
11
+
12
+ The booking **logic** — the step model, the shared selection state, the exact
13
+ `@wix` SDK sequence, and the schema-driven form — is framework-agnostic and lives
14
+ in **`./FLOW.md`**. Read it first. The astro guides below are the astro *wiring* of
15
+ that logic; the code examples under `<SKILL_ROOT>/references/astro/templates/bookings/`
16
+ are React (the astro islands use them directly). For an own/static build, the wiring guide is
17
+ `../custom/bookings/WIRING.md` (same logic, client-side `@wix/sdk`).
9
18
 
10
19
  ## Scope routing
11
20
 
12
21
  | Scope | Phase | Reference |
13
22
  |-------|-------|-----------|
14
- | `seed` | Seed (service creation via Wix Bookings REST API) | `./SERVICES_DATA.md` |
15
- | `components` | Components: `ServiceCard.astro`, `AvailabilityCalendar.tsx`, `BookingForm.tsx`, `ServiceBookingFlow.tsx`, `ManageBooking.tsx` (`SeoTags.astro` is pre-copied — see Templates) | `../astro/bookings/COMPONENTS.md` |
16
- | `pages` | Pages: `/services` listing, `/services/[slug]` detail, `/booking-confirmation`, `/manage-booking` | `../astro/bookings/SERVICES_PAGES.md` |
23
+ | `seed` | Seed — create services (and staff) via the Wix Bookings REST API | `./SERVICES_DATA.md` |
24
+ | `components` | Components — the client islands (week calendar, schema-driven form, the flow coordinator) | `./FLOW.md` + `../astro/bookings/COMPONENTS.md` |
25
+ | `pages` | Pages — `/services` listing, `/services/[slug]` detail, `/booking-confirmation`, + nav/home links | `./FLOW.md` + `../astro/bookings/SERVICES_PAGES.md` |
17
26
 
18
- ## Files this vertical creates / contributes
27
+ ## Templates — read and adapt (don't invent)
19
28
 
20
- See `<SKILL_ROOT>/references/verticals/bookings.md` frontmatter.
29
+ Canonical examples live at `<SKILL_ROOT>/references/astro/templates/bookings/`.
30
+ Your `components` and `pages` scopes **read these and adapt them** — adapt brand
31
+ copy, headings, and styling; keep the SDK calls, payload shapes, and the data
32
+ flow (re-authoring the SDK wiring from scratch is the main source of API-shape bugs).
21
33
 
22
- ## Pre-return file-existence assertion (pages scope)
34
+ Components (`components` scope — TSX only, no CSS):
35
+ - `<SKILL_ROOT>/references/astro/templates/bookings/AvailabilityCalendar.tsx` — the week calendar (week strip → the day's slots; APPOINTMENT via `availabilityTimeSlots`, CLASS via `eventTimeSlots`).
36
+ - `<SKILL_ROOT>/references/astro/templates/bookings/BookingForm.tsx` — the schema-driven form (renders the `@wix/forms` field list, keys values by `target`) that drives `bookingDriver.book()`.
37
+ - `<SKILL_ROOT>/references/astro/templates/bookings/ServiceBookingFlow.tsx` — the coordinator island (holds the selected slot; calendar → form → redirect).
23
38
 
24
- Before returning `status: "complete"` from the `pages` scope, verify all route files exist on disk:
25
- - `src/pages/services/index.astro`
26
- - `src/pages/services/[slug].astro`
27
- - `src/pages/booking-confirmation.astro`
28
- - `src/pages/manage-booking.astro`
39
+ Pages (`pages` scope):
40
+ - `<SKILL_ROOT>/references/astro/templates/bookings/ServiceCard.astro`
41
+ - `<SKILL_ROOT>/references/astro/templates/bookings/services/index.astro`, `<SKILL_ROOT>/references/astro/templates/bookings/services/[slug].astro`
42
+ - `<SKILL_ROOT>/references/astro/templates/bookings/booking-confirmation.astro`
29
43
 
30
- If any declared file is missing, return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<expected path>" }]` rather than claiming success.
44
+ ### Pre-copied by the orchestrator (do NOT write these yourself)
45
+ Mechanical, brand-agnostic — the orchestrator copies them before dispatch (BUILD-astro.md § build wave). Rely on them at the listed paths:
46
+ - `src/components/bookingDriver.ts` ← `<SKILL_ROOT>/references/astro/templates/bookings/bookingDriver.ts` — the booking SDK sequence (`book()`, `navigateToCheckout()`). The islands import it; never re-author it.
47
+ - `src/components/SeoTags.astro` ← `<SKILL_ROOT>/references/astro/templates/bookings/SeoTags.astro` — renders `service.seoData.tags`; imported by `services/[slug].astro`.
48
+ - `src/styles/components-bookings.css` ← `<SKILL_ROOT>/references/astro/templates/bookings/components-bookings.css` — the flow's component classes.
31
49
 
32
- ## Templates
50
+ If a pre-copied file is missing at runtime, that's an orchestrator-side bug — return `status: "partial"` with `errors: [{code: "UTILITY_TEMPLATE_NOT_PRECOPIED", path: "<missing>"}]`; do not author your own version.
33
51
 
34
- Canonical templates live at `<SKILL_ROOT>/references/astro/templates/bookings/`. Your `components` and `pages` scopes **read these and adapt them — don't invent markup or SDK wiring** (the booking/confirm/availability/waitlist wiring is subtle and was verified against a live site; re-authoring from scratch is a common source of API-shape bugs). Adapt brand copy, headings, and styling; keep the SDK calls and the data flow.
52
+ ## Pre-return file-existence assertion (pages scope)
35
53
 
36
- Components (`components` scope — `.tsx`/`.astro`, no CSS):
37
- - `…/templates/bookings/ServiceCard.astro`
38
- - `…/templates/bookings/AvailabilityCalendar.tsx` — branches on `serviceType` (APPOINTMENT → `availabilityTimeSlots`, CLASS → `eventTimeSlots`); capacity + instructor + full→waitlist
39
- - `…/templates/bookings/BookingForm.tsx` — `createBooking` → `/api/confirm-booking`; party size; waitlist on full
40
- - `…/templates/bookings/ServiceBookingFlow.tsx` — coordinator (threads `serviceType`)
41
- - `…/templates/bookings/ManageBooking.tsx` — cancel via anonymous token (used by `manage-booking.astro`)
54
+ Before returning `status: "complete"` from `pages`, verify on disk:
55
+ - `src/pages/services/index.astro`
56
+ - `src/pages/services/[slug].astro`
57
+ - `src/pages/booking-confirmation.astro`
42
58
 
43
- Pages (`pages` scope):
44
- - `…/templates/bookings/services/index.astro`, `…/services/[slug].astro`
45
- - `…/templates/bookings/booking-confirmation.astro`, `…/manage-booking.astro`
59
+ If any declared file is missing, return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<expected path>" }]`.
46
60
 
47
- ### Pre-copied by the orchestrator (do NOT write these yourself)
48
- These are mechanical (no brand content) — the orchestrator copies them before dispatch (BUILD-astro.md § Step 4.5). Just rely on them at the listed paths:
49
- - `src/styles/components-bookings.css` ← `…/templates/bookings/components-bookings.css`
50
- - `src/components/SeoTags.astro` ← `…/templates/bookings/SeoTags.astro` (renders `service.seoData.tags`; imported by `services/[slug].astro`)
51
- - `src/pages/api/confirm-booking.ts` ← `…/templates/bookings/api/confirm-booking.ts` (elevated `confirmBooking` — holds the seat)
52
- - `src/pages/api/waitlist.ts` ← `…/templates/bookings/api/waitlist.ts` (elevated native v1 waitlist register)
61
+ ## Dependencies (Setup installs these — see SETUP.md)
53
62
 
54
- If a pre-copied file is missing at runtime, that's an orchestrator-side bug — return `status: "partial"` with `errors: [{code: "UTILITY_TEMPLATE_NOT_PRECOPIED", path: "<missing>"}]`; do not author your own version.
63
+ `@wix/bookings @wix/essentials @wix/forms @wix/redirects @wix/auto_sdk_ecom_cart-v-2`.
64
+ `@wix/forms` renders the booking-form schema; `@wix/redirects` + `@wix/auto_sdk_ecom_cart-v-2` run the cart/checkout sequence.
55
65
 
56
66
  ## CSS ownership — bookings pack
57
67
 
58
- Bookings-specific component CSS lives in `src/styles/components-bookings.css` (pre-copied by the orchestrator from the skill template — see above), NOT in the designer's `global.css`. Classes the pack owns:
59
-
60
- - `.service-card`, `.service-card-image`, `.service-card-meta`, `.service-card-price` — the service card itself, including image containment, price badge, and duration tag.
61
- - `.service-grid` — the layout that lists service cards on `/services`. Include `display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--spacing-xl);` plus View-Transitions opacity fade.
62
- - `.availability-calendar` — the date-picker shell wrapping the calendar widget.
63
- - `.time-slot`, `.time-slot--available`, `.time-slot--selected`, `.time-slot--full` (full CLASS session → waitlist), plus the `.time-slot-time` / `.time-slot-capacity` spans inside each slot button.
64
- - `.booking-form` (+ `.booking-form-note`) — the booking/waitlist form revealed after a slot is selected.
65
- - `.manage-booking`, `.manage-booking-summary`, `.manage-booking-status`, `.manage-booking-note`, `.manage-booking-link` — the `/manage-booking` view/cancel UI.
66
-
67
- If `global.css` ships a partial rule for any class above, flag it in your return JSON's `errors` array (`{code: "GLOBAL_CSS_LEAK", class: "<name>"}`) and override with the complete rule in `components-bookings.css`.
68
+ Bookings-specific CSS lives in `src/styles/components-bookings.css` (pre-copied — see above), NOT in `global.css`. Classes the pack owns: `.service-card*`, `.service-grid`, `.availability-*`, `.time-slot*`, `.booking-*`. If `global.css` ships a partial rule for any of these, flag it (`{code:"GLOBAL_CSS_LEAK", class:"<name>"}`) and override in `components-bookings.css`.
68
69
 
69
70
  ## Bookings-specific failure modes
70
71
 
71
72
  | Wrong | Right |
72
73
  |-------|-------|
73
- | Treat `createBooking` as the whole booking — stop after it returns | `createBooking` leaves the booking **`CREATED`**, which **does not hold a seat or block availability** (per the [lifecycle docs](https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/introduction): only `CONFIRMED` blocks availability). Without a follow-up **`confirmBooking`**, the class overbooks and capacity never drops. After createBooking returns `CREATED`, call `bookings.confirmBooking(bookingId, revision, { paymentStatus: "NOT_PAID", flowControlSettings: { checkAvailabilityValidation: true } })`. `confirmBooking` needs Manage Bookings → run it **server-side, elevated** (`auth.elevate`), not from the visitor client. |
74
- | Build the waitlist on `createBooking` (expecting a `WAITING_LIST` status) | `createBooking` has **no** waitlist option (verified: only `participantNotification`/`sendSmsReminder`); a full session returns `SESSION_CAPACITY_EXCEEDED`. The native waitlist is the **legacy v1** API `POST /bookings/v1/waitlist/register` (there is no v2/v3 waitlist). It needs the **v1-encoded session id** as `waitingResource` (surfaced in the `SESSION_CAPACITY_EXCEEDED` message: *"…on session `<id>`"*) **+ a Wix `contactId`** (resolve via Contacts API by email, create if missing), and the **Manage Bookings** scope → server-side elevated. Class/EVENT only. |
75
- | Call the v1 waitlist (or any Manage-Bookings REST) with `auth.elevate(async () => fetchWithAuth(...))` | `auth.elevate()` only grants permissions for a real **SDK function** (it reads the method's required-permission metadata); a plain closure isn't one, so it runs **un-elevated (visitor)** and 403s. For raw REST with no SDK wrapper, elevate the authenticated fetch itself: `const ef = auth.elevate(httpClient.fetchWithAuth); await ef(url, init)`. (For calls that *do* have an SDK method — `confirmBooking`, `getAnonymousActionToken` — elevate the method directly.) |
76
- | Pass the v3 `eventId` as the waitlist `waitingResource` | The v1 waitlist can't decode a v3 GUID (`"id … cannot be decoded"`). Use the **v1-encoded session id** (e.g. `2mmoW0vw…`) from the `SESSION_CAPACITY_EXCEEDED` message. |
77
- | Use Services V1 API endpoint (`/bookings/v1/catalog/services`) | Services V2 is at `POST https://www.wixapis.com/_api/bookings/v2/services`. V1 has a different (nested `info.*`) payload shape. |
78
- | Nest service fields under `info` (`info.name`, `info.description`) | V2 uses flat fields: `name`, `description`, `tagLine` at the top level of the `service` object. |
79
- | Use `payment.fixed.price.amount` | V2 uses `payment.fixed.price.value` (a string like `"75.00"`). `amount` is the V1 field name. |
80
- | Omit `defaultCapacity` when creating a service | Required in V2. Set to `1` for APPOINTMENT; use participant count for CLASS. |
81
- | Omit `onlineBooking` when creating a service | Required in V2. At minimum `{ "enabled": true }`. |
82
- | Omit `sessionDurations` for an APPOINTMENT service | Required for APPOINTMENT: `schedule.availabilityConstraints.sessionDurations: [<minutes as int>]`. Do NOT specify for CLASS. |
83
- | Use `availability.queryAvailability` or `availability` SDK namespace | Branch by service type: APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots()`; CLASS → **`eventTimeSlots.listEventTimeSlots()`** (a **different namespace** — `eventTimeSlots`, not `availabilityTimeSlots` — with plural `serviceIds` + `includeNonBookable`). The `availability` namespace does not exist in `@wix/bookings`. |
84
- | Pass UTC ISO strings to `listAvailabilityTimeSlots` | The method expects **local date strings** in `YYYY-MM-DDTHH:mm:ss` format (no `Z`), with a separate `timeZone` parameter. |
85
- | Read service slug from `service.slug` | V2 response puts slug at `service.mainSlug.name`. Fall back to `service.supportedSlugs[0].name`, then `service.id`. |
86
- | Call `queryServices({ query: { filter, paging } })` (object form) | In `@wix/bookings` the object form returns **0 items with no error**. Use the **query builder**: `await services.queryServices().limit(100).find()` → `result.items`. For a slug, fetch the (small) catalog and match `mainSlug.name` in JS, or `.eq("mainSlug.name", slug).find()`. |
87
- | Build a `createClient`/`OAuthStrategy` in `.astro` SSR frontmatter keyed off `import.meta.env.*_WIX_CLIENT_ID` | The `*_WIX_CLIENT_ID` env vars are client-only in `@wix/astro` and `undefined` at server render → `createClient` throws → HTTP 500 (only in the deployed runtime; dev has `.env.local`). SSR reads use `@wix/essentials` `auth.elevate(services.queryServices)()…` — the same ambient pattern as the CMS pages. |
88
- | Read the client ID in a browser island via `import.meta.env.*_WIX_CLIENT_ID` | Client islands import `{ WIX_CLIENT_ID } from "astro:env/client"`. `import.meta.env` is `undefined` in the browser bundle → `OAuthStrategy` sends an empty `client_id` → `oauth2/token` `400`. The var name is `WIX_CLIENT_ID` (no `PUBLIC_`). |
89
- | Read service name from `service.info.name` | V2 uses flat `service.name` (not `service.info.name`). Same for `description` and `tagLine`. |
90
- | Read duration from `service.schedule.sessions.duration` | V2 puts duration at `service.schedule.availabilityConstraints.sessionDurations[0]` (array of ints). |
91
- | Read media URL from `service.info.images[0].url` | V2 media: `service.media.mainMedia.image.url`. |
92
- | Use `import { availability } from '@wix/bookings'` | Use `import { availabilityTimeSlots } from '@wix/bookings'`. |
93
- | Call `availability.queryAvailability` server-side on every render | Availability is real-time and session-specific — always call from the client-side React island, NOT from `.astro` frontmatter SSR. |
94
- | Create a booking without `totalParticipants` | `createBooking` requires `totalParticipants: 1` (or `participantsChoices`). |
95
- | Read booking ID from `result.booking.id` | The booking ID field is `result.booking._id` (underscore prefix). |
96
- | Redirect to `/thank-you` (ecom route) after booking | Redirect to `/booking-confirmation?bookingId=<_id>`. The ecom thank-you page is unrelated to bookings. |
97
- | Re-fetch the booking with `getBooking` from the confirmation page | Don't. Pass booking details (bookingId, startDate, service name) as URL query params during the client-side redirect after `createBooking` succeeds, and render from those. (A server re-fetch of someone else's booking is out of scope.) NB: this is **not** because `auth.elevate` is unavailable — see next row. |
98
- | Assume `@wix/essentials` `auth.elevate` doesn't work in headless | It does. **Two different `elevate`s:** `@wix/essentials` `auth.elevate(fn)` works in `@wix/astro` SSR (it's how the listing/detail/CMS pages read data) — use it. What's unavailable is the **`wixClient.auth.elevate`** instance method on a hand-built `createClient({auth: OAuthStrategy})` (a Velo/Blocks API). Don't conflate them. |
99
- | Mount `AvailabilityCalendar` without `client:only="react"` | Calendar must be fully client-side — SSR renders before timezone is known and breaks slot display. Always use `client:only="react"`. |
100
- | Omit try/catch on `createBooking` | Booking creation can fail with 409 (slot taken) between slot fetch and submit. Catch and surface a friendly message; do not crash the page. |
101
- | Use `service.id` to pass the service ID | The Wix SDK uses `_id` (with underscore) for all entity IDs. The service's ID is `service._id`. Using `service.id` will be `undefined`. |
102
- | Read `resource._id` from `listAvailabilityTimeSlots` response | `availableResources` is EMPTY by default in list results. Omit `resource` from `createBooking` — Wix auto-assigns a resource during confirmation. |
103
- | Use `timeSlot.slot.startDate` to read slot time | Slot fields are at the TOP LEVEL — there is no nested `slot` on the list response. APPOINTMENT slots: `timeSlot.localStartDate`, `timeSlot.localEndDate`, `timeSlot.scheduleId`. |
104
- | Treat CLASS (event) slots like APPOINTMENT slots — guard clicks on `scheduleId`, book with `{scheduleId, startDate}` | CLASS event slots carry their session id at **`timeSlot.eventInfo.eventId`** and have **no `scheduleId`** (and no top-level `startDate`). Guarding the slot-click on `scheduleId` drops every CLASS click; the appointment-shaped `createBooking` payload doesn't apply. For CLASS: guard on `eventId`, and book with `bookedEntity.slot = { serviceId, eventId }` (Wix derives start/end/timezone/resource/location from the event). Keep `{ scheduleId, startDate, endDate, timezone, location }` for APPOINTMENT only. |
105
- | Pass `hidden: true` services to the listing | Always filter with `{ hidden: false }` in your `queryServices` filter to exclude hidden services from the public listing. |
106
- | Use `../../../` for imports in `[slug].astro` | `src/pages/services/[slug].astro` is two directories deep inside `src/`. Import Layout and components with `../../layouts/Layout.astro` and `../../components/…` — not three `../` which would escape `src/`. |
74
+ | Stop after `createBooking` returns | `createBooking` leaves the booking **`CREATED`**. The seat is held by the **ecom Cart V2**: continue `createCart → calculateCart → isCheckoutRequired ? hosted-checkout redirect : placeOrder`. The whole sequence is in `bookingDriver.ts` — use it. |
75
+ | Hardcode `selectedPaymentOption: "ONLINE"` on `createBooking` | **Derive** it from the service: `online && !inPerson → "ONLINE"`, `!online && inPerson → "OFFLINE"`, else `"ONLINE"`. A free / pay-in-person service is the only kind that reaches `placeOrder`, and booking it `ONLINE` makes the cart reject it with **`INSUFFICIENT_INVENTORY`** (`available_quantity: 0`). `bookingDriver.ts` derives this for you (don't pass `selectedPaymentOption` to `book()`). |
76
+ | Pass `contactDetails` to `createBooking` | Pass **`formSubmission`**, keyed by each field's **`target`** (default booking form: snake_case `first_name`/`last_name`/`email`/`phone`). `contactDetails` is what Wix derives back onto the response. |
77
+ | Hardcode the booking-form fields | Render the form **schema-driven**: read `service.form._id`'s fields via `@wix/forms`, render by `componentType`, key values by `target` (same renderer as `../astro/forms/CONTACT_FORM.md`). |
78
+ | Render every form field as a text input (default unknown types to `TEXT_INPUT`) | Render **only** fields with a recognized string `componentType` (`TEXT_INPUT`/`PHONE_INPUT`/`DROPDOWN`). **Skip complex, object-valued fields** (e.g. the default booking form's multi-line `ADDRESS`) — sending a string for them fails `createBooking` with **"must be object"**. Only `first_name`/`last_name`/`email` are enforced, so skipping optional complex fields is safe. |
79
+ | Call `queryServices` without an `appId` filter | Always include `appId` (`13d21c63-…`) in the filter. List: `queryServices({ query:{ filter:{ appId }, paging }, conditionalFields:["STAFF_MEMBER_DETAILS"] }).find()`. Single: the `.eq("mainSlug.name", slug).eq("appId", …).limit(1).find()` builder chain. |
80
+ | Build `createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) })` in `.astro` SSR | The `*_WIX_CLIENT_ID` env var is client-only → `undefined` at server render → 500. SSR reads use the ambient `@wix/essentials`: `auth.elevate(services.queryServices)(...)`. |
81
+ | Build an `OAuthStrategy` client inside an astro browser island | Astro islands call the `@wix` modules **ambiently** (the `@wix/astro` visitor client, like the ecom `CartView`) — no `createClient`. (Own/static builds DO use `OAuthStrategy` — `../custom/bookings/WIRING.md`.) |
82
+ | Pass UTC ISO strings to `listAvailabilityTimeSlots` | Use **local** date strings `YYYY-MM-DDThh:mm:ss` (no `Z`) + a separate `timeZone`. |
83
+ | `availability.queryAvailability` / `import { availability }` | APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: <string> })`; CLASS → `eventTimeSlots.listEventTimeSlots({ serviceIds: [<string>] })`. The `availability` namespace does not exist. |
84
+ | Read slot time from `timeSlot.slot.startDate` | Slot fields are at the **top level**: `localStartDate`, `localEndDate`, `scheduleId` (APPOINTMENT). CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`. |
85
+ | Dump every slot in one flat, time-only grid | Show a **week calendar**: a day strip → the picked day's times. A flat grid hides which day a slot is on. |
86
+ | Read service slug from `service.slug` / name from `service.info.name` | V2 is flat: slug `service.mainSlug.name` (fallback `supportedSlugs[0].name`), `service.name`, `service.description`, `service.tagLine`; duration `service.schedule.availabilityConstraints.sessionDurations[0]`; price `service.payment.fixed.price.value` (string); media `service.media.mainMedia.image.url`. |
87
+ | Use `service.id` / `result.booking.id` | Entity ids are `_id` (underscore): `service._id`, `booking._id`. |
88
+ | Mount the calendar/form without `client:only="react"` | Availability + booking are timezone/session-specific — always `client:only="react"`. SSR only the read pages (catalog/detail) for SEO. |
89
+ | Omit try/catch on the booking | `createBooking` can reject (e.g. a slot taken between fetch and submit, or strict phone validation). Catch and surface a friendly message; don't crash. |
90
+ | Ship a location selector, staff filter, payment breakdown, CLASS sign-up, or waitlist | Out of v1 — appointment booking only. |