@wix/agent-skills 1.19.2 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/install/fast-path.mjs +3 -1
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +13 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +227 -56
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +165 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +48 -15
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +129 -11
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +36 -6
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +190 -24
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +5 -1
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +59 -6
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +43 -2
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Wix Service Plugin (SPI) Builder
|
|
3
3
|
|
|
4
|
-
Service plugins are a set of APIs defined by Wix that let you inject custom logic into the existing backend flows of Wix business solutions or introduce entirely new flows. When you implement a service plugin, Wix calls your custom functions during specific flows. Common use cases include eCommerce customization (shipping, fees, payment settings, validations)
|
|
4
|
+
Service plugins are a set of APIs defined by Wix that let you inject custom logic into the existing backend flows of Wix business solutions or introduce entirely new flows. When you implement a service plugin, Wix calls your custom functions during specific flows. Common use cases include eCommerce customization (shipping, fees, payment settings, validations), Bookings customization (staff sorting, booking validation, policies, pricing, availability), Realtime channel permissions, and exposing tools to the Wix AI assistant.
|
|
5
5
|
|
|
6
6
|
## Scaffold
|
|
7
7
|
|
|
@@ -16,12 +16,19 @@ Use `wix generate --params` with `extensionType: SERVICE_PLUGIN`. `pluginType` i
|
|
|
16
16
|
| `ECOM_PAYMENT_SETTINGS` | Payment Settings | No |
|
|
17
17
|
| `GIFT_CARDS_PROVIDER` | Gift Cards Provider | **Yes** |
|
|
18
18
|
| `STAFF_SORTING_PROVIDER` | Bookings Staff Sorting | No |
|
|
19
|
+
| `BOOKINGS_VALIDATION_PROVIDER` | Bookings Validation | No |
|
|
20
|
+
| `BOOKING_POLICY_PROVIDER` | Booking Policy | No |
|
|
21
|
+
| `BOOKINGS_PRICING_PROVIDER` | Bookings Pricing Provider | No |
|
|
22
|
+
| `AVAILABILITY_TIME_SLOTS_CONFIGURATION_PROVIDER` | Availability Time Slots Configuration | No |
|
|
19
23
|
| `REALTIME_PERMISSIONS_PROVIDER` | Realtime Permissions Provider | No |
|
|
24
|
+
| `TOOLS_PROVIDER_CONFIG` | Tools Provider | No |
|
|
20
25
|
|
|
21
26
|
> **Singular types** — `ECOM_SHIPPING_RATES`, `ECOM_DISCOUNTS_TRIGGER`, and `GIFT_CARDS_PROVIDER` are **singular**: only one component of each type is allowed per app. Never scaffold or include two components of the same singular type in the same app payload.
|
|
22
27
|
|
|
23
28
|
`name` must be lowercase alphanumeric + hyphens, max 19 characters. The CLI generates the folder, `plugin.ts`, the builder file, the UUID, and the `src/extensions.ts` registration with the appropriate builder method for the SPI type. Some SPI types (e.g., `ECOM_SHIPPING_RATES`) get a `description` placeholder field in the generated builder — replace it with your real copy.
|
|
24
29
|
|
|
30
|
+
> **Missing dependency after scaffolding:** `wix generate` adds the SPI's SDK package (e.g. `@wix/bookings`, `@wix/realtime`) to `package.json`, but does not always install it into `node_modules`. If `tsc`/`wix build` reports the import as unresolved right after scaffolding, run `npm install <package>` for just that package (no need for a full reinstall) before debugging further.
|
|
31
|
+
|
|
25
32
|
## References
|
|
26
33
|
|
|
27
34
|
**You MUST read the relevant reference document before implementing an SPI**, and **call `ReadFullDocsMethodSchema`** with the docs URL it points at to get the exact request/response types — **do NOT edit code until you have the schema**. If the schema alone isn't enough, follow up with `ReadFullDocsArticle` on the same URL for prose explanations and additional code examples. Each reference also contains the correct imports, handler signatures, response structures, and a worked example.
|
|
@@ -35,6 +42,12 @@ Use `wix generate --params` with `extensionType: SERVICE_PLUGIN`. `pluginType` i
|
|
|
35
42
|
| Shipping Rates | [SHIPPING-RATES.md](service-plugin/SHIPPING-RATES.md) |
|
|
36
43
|
| Validations | [VALIDATIONS.md](service-plugin/VALIDATIONS.md) |
|
|
37
44
|
| Bookings Staff Sorting | [BOOKINGS-STAFF-SORTING.md](service-plugin/BOOKINGS-STAFF-SORTING.md) |
|
|
45
|
+
| Bookings Validation | [BOOKINGS-VALIDATION.md](service-plugin/BOOKINGS-VALIDATION.md) |
|
|
46
|
+
| Booking Policy | [BOOKING-POLICY.md](service-plugin/BOOKING-POLICY.md) |
|
|
47
|
+
| Bookings Pricing Provider | [BOOKINGS-PRICING.md](service-plugin/BOOKINGS-PRICING.md) |
|
|
48
|
+
| Availability Time Slots Configuration | [AVAILABILITY-TIME-SLOTS-CONFIGURATION.md](service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md) |
|
|
49
|
+
| Realtime Permissions Provider | [REALTIME-PERMISSIONS.md](service-plugin/REALTIME-PERMISSIONS.md) |
|
|
50
|
+
| Tools Provider | [TOOLS_PROVIDER.md](service-plugin/TOOLS_PROVIDER.md) |
|
|
38
51
|
|
|
39
52
|
## Implementation Pattern
|
|
40
53
|
|
|
@@ -140,7 +140,7 @@ clears `isStoppedByFocus`.
|
|
|
140
140
|
|
|
141
141
|
## 3. Add the Play/Pause Button
|
|
142
142
|
|
|
143
|
-
Create play/pause icons that visually match the component's style. Use simple recognizable shapes — a triangle for play, two rectangles for pause — implemented as inline SVG so there is no external icon dependency. Size
|
|
143
|
+
Create play/pause icons that visually match the component's style. Use simple recognizable shapes — a triangle for play, two rectangles for pause — implemented as inline SVG so there is no external icon dependency. Size and color of SVG should be controlled by CSS variables declared on the button level: --icon-size and --icon-color.
|
|
144
144
|
|
|
145
145
|
Position the button absolutely so it overlays the content without pushing other elements out of place:
|
|
146
146
|
|
|
@@ -152,14 +152,14 @@ Position the button absolutely so it overlays the content without pushing other
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
.playButton {
|
|
155
|
+
--icon-size: 20px;
|
|
156
|
+
--icon-color: #ffffff;
|
|
155
157
|
position: absolute;
|
|
156
158
|
inset-inline-end: 5px;
|
|
157
159
|
inset-block-start: 5px;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
/* Design-state selectors: pair native pseudo-class with editor-injected modifier class.
|
|
161
|
-
The editor applies the modifier class (e.g. my-animation-play-button--hover) when
|
|
162
|
-
the site owner previews that state in the design panel, so both selectors must exist. */
|
|
162
|
+
/* Design-state selectors: pair native pseudo-class with editor-injected modifier class. */
|
|
163
163
|
.playButton:global(.my-animation-play-button--hover),
|
|
164
164
|
.playButton:hover {
|
|
165
165
|
/* e.g. background: rgba(255, 255, 255, 1); */
|
|
@@ -257,8 +257,7 @@ const ComponentNamePreview: FC<ComponentProps<typeof Component>> = (props) => {
|
|
|
257
257
|
};
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
In preview mode (`isEditMode` is `false`) → both use the user's configured values.
|
|
260
|
+
Design mode stops autoplay and exposes the control; preview uses the user's values.
|
|
262
261
|
|
|
263
262
|
## Checklist
|
|
264
263
|
|
|
@@ -267,6 +266,8 @@ In preview mode (`isEditMode` is `false`) → both use the user's configured val
|
|
|
267
266
|
present only when repeat behavior is supported.
|
|
268
267
|
- [ ] Reduced motion starts paused and never restarts playback automatically.
|
|
269
268
|
- [ ] The play/pause button is a fully wired named part with a stable accessible name.
|
|
269
|
+
- [ ] Both icons inherit button-owned `--icon-size` for width and height and
|
|
270
|
+
`--icon-color` via `color` and `currentColor` for fill or stroke.
|
|
270
271
|
- [ ] Hover has a paired editor design state; `:focus-visible` remains a
|
|
271
272
|
standalone keyboard indicator.
|
|
272
273
|
- [ ] Hover-only visibility changes behavior, not the button's editable styling surface.
|
|
@@ -87,6 +87,10 @@ additionalFees.provideHandlers({
|
|
|
87
87
|
});
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Manual Setup Required
|
|
91
|
+
|
|
92
|
+
None — confirmed live. Once the app is installed and released, Wix calls this plugin automatically on every cart/checkout calculation; no dashboard configuration or extra API call is needed to "turn it on."
|
|
93
|
+
|
|
90
94
|
## Key Implementation Notes
|
|
91
95
|
|
|
92
96
|
1. **Elevate permissions for API calls** - Use `auth.elevate` from `@wix/essentials` when calling Wix APIs from service plugins
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Availability Time Slots Configuration Service Plugin Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Availability Time Slots Configuration SPI lets you customize how Wix calculates available time slots for booking services: duration, split interval, buffer time, locations, and required/available resources. Wix calls your `listAvailabilityTimeSlotConfigurations` handler whenever a customer views available times or books a service. [Resources](https://dev.wix.com/docs/api-reference/business-solutions/bookings/resources/resources-v2/introduction) must already exist before time slot calculation runs.
|
|
6
|
+
|
|
7
|
+
## ⚠️ `resourceTypeId` is not a placeholder you can invent
|
|
8
|
+
|
|
9
|
+
`resourceTypes[].resourceTypeId` must be a real resource type ID or Wix rejects every availability query for the service — confirmed live: a made-up GUID here 500s `List Availability Time Slots` and fails booking creation with `FAILED_VALIDATING_AVAILABILITY`, for every service on the site, not just the one you're configuring. If the service is staffed (the common case), use the universal Wix Bookings staff-member resource type ID, which is the same across every site — it is not something you provision or look up per site:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
const STAFF_MEMBER_RESOURCE_TYPE_ID = '1cd44cf8-756f-41c3-bd90-3e2ffcaf1155';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For a non-staff resource (a room, piece of equipment, etc.), get the real ID from [Get Resource](https://dev.wix.com/docs/api-reference/business-solutions/bookings/resources/resources-v2/get-resource) (`resource.typeId`) — never fabricate one.
|
|
16
|
+
|
|
17
|
+
## ⚠️ `resourceTypes[].resourceIds` — populate it, don't leave it implicit
|
|
18
|
+
|
|
19
|
+
`resourceIds` ("IDs of resources available for booking within this resource type... may be a subset of all resources in the type") tells Wix *which* resources of that type are valid for this specific service. Omitting it (relying on `resourceTypeId` alone) was part of the same live 500 chain above — once `resourceTypeId` and `scheduleId` were both fixed, availability queries still failed until `resourceIds` was populated too. Get the service's actual assigned staff from [Get Service](https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/get-service)'s `service.staffMemberIds`.
|
|
20
|
+
|
|
21
|
+
## ⚠️ `scheduleId` is required — omitting it also 500s
|
|
22
|
+
|
|
23
|
+
The response schema requires `scheduleId` (confirmed live: leaving it out 500s the same as a bad `resourceTypeId`), but it's easy to miss since it's not mentioned in the request/response summary above. Fetch it from the service itself — [Get Service](https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/get-service)'s `service.schedule._id` (REST docs call this field `id`; the SDK's `Service.schedule` type names it `_id`):
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { auth } from "@wix/essentials";
|
|
27
|
+
import { services } from "@wix/bookings";
|
|
28
|
+
|
|
29
|
+
const elevatedGetService = auth.elevate(services.getService);
|
|
30
|
+
const service = await elevatedGetService(serviceId);
|
|
31
|
+
const scheduleId = service.schedule?._id;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## ⚠️ `locations[]._id` is required when `locationType` is `BUSINESS`
|
|
35
|
+
|
|
36
|
+
The schema notes `_id` is "returned only for business locations" — read that as *required whenever `locationType: BUSINESS`*, not optional. Omitting it was the third and last piece of the same live 500 chain. Get the service's actual configured locations from [Get Service](https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/get-service)'s `service.locations[]` (each has its own `_id`) rather than hardcoding a bare `{ locationType: "BUSINESS" }`.
|
|
37
|
+
|
|
38
|
+
## Manual Setup Required
|
|
39
|
+
|
|
40
|
+
None for activation — Wix calls this plugin automatically once installed and released. But get all three fields above right, or every availability query for every service on the site fails with a bare 500, not just a validation error on the one service you're configuring.
|
|
41
|
+
|
|
42
|
+
## Request and Response Schema
|
|
43
|
+
|
|
44
|
+
Before implementing, call `ReadFullDocsMethodSchema` on the docs URL to get the full request/response types.
|
|
45
|
+
|
|
46
|
+
| Handler | Docs URL |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `listAvailabilityTimeSlotConfigurations` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/availability-time-slots-configuration-service-plugin/list-availability-time-slot-configurations?apiView=SDK |
|
|
49
|
+
|
|
50
|
+
The request's `services` array (up to 100 entries) lists each service to configure, with the customer's `customerChoices` (selected duration variant and/or add-ons) — the same service GUID can appear more than once with different choices. Your response's `configurations` array must contain exactly one configuration per requested entry, **in the same order**, with `resourceTypes`, `locations`, and `scheduleId` fully populated.
|
|
51
|
+
|
|
52
|
+
For services with a bookable duration **range** (the customer picks how long to book), return `durationUnit` (`HOUR` or `DAY`) with `minDuration`/`maxDuration` instead of a fixed `duration`; for hourly ranges also return `duration.defaultInMinutes` as the minimum.
|
|
53
|
+
|
|
54
|
+
## Example: Duration From Add-Ons, Single Resource Type
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { availabilityTimeSlotsConfiguration } from "@wix/bookings/service-plugins";
|
|
58
|
+
import { auth } from "@wix/essentials";
|
|
59
|
+
import { services } from "@wix/bookings";
|
|
60
|
+
|
|
61
|
+
// Universal Wix Bookings staff-member resource type ID — same on every site, not per-site data.
|
|
62
|
+
const STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
|
|
63
|
+
|
|
64
|
+
availabilityTimeSlotsConfiguration.provideHandlers({
|
|
65
|
+
listAvailabilityTimeSlotConfigurations: async (payload) => {
|
|
66
|
+
const { request } = payload;
|
|
67
|
+
// services is optional on the request type — default it, or the .map()
|
|
68
|
+
// calls below fail `tsc` with "possibly undefined."
|
|
69
|
+
const { services: requestedServices = [] } = request;
|
|
70
|
+
|
|
71
|
+
// scheduleId is required in the response but isn't in the request — fetch it per service.
|
|
72
|
+
const elevatedGetService = auth.elevate(services.getService);
|
|
73
|
+
const serviceById = new Map(
|
|
74
|
+
await Promise.all(
|
|
75
|
+
requestedServices.map(async ({ serviceId }) =>
|
|
76
|
+
[serviceId, serviceId ? await elevatedGetService(serviceId) : undefined] as const
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const configurations = requestedServices.map(({ serviceId, customerChoices }) => {
|
|
82
|
+
const baseDuration = 60;
|
|
83
|
+
const addOnMinutes = (customerChoices?.addOnIds ?? []).length * 15;
|
|
84
|
+
const service = serviceById.get(serviceId);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
serviceId,
|
|
88
|
+
resourceTypes: [
|
|
89
|
+
{
|
|
90
|
+
resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID,
|
|
91
|
+
resourceIds: service?.staffMemberIds ?? [],
|
|
92
|
+
numberOfResourcesRequired: 1,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
locations: (service?.locations ?? []).map((location) => ({
|
|
96
|
+
_id: location._id,
|
|
97
|
+
locationType: availabilityTimeSlotsConfiguration.LocationType.BUSINESS,
|
|
98
|
+
})),
|
|
99
|
+
splitIntervalInMinutes: baseDuration + addOnMinutes,
|
|
100
|
+
duration: { defaultInMinutes: baseDuration + addOnMinutes },
|
|
101
|
+
bufferTimeInMinutes: 10,
|
|
102
|
+
scheduleId: service?.schedule?._id,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { configurations };
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Key Implementation Notes
|
|
112
|
+
|
|
113
|
+
1. **One configuration per requested entry, same order** — the response array must line up positionally with the request's `services`, even when the same service GUID repeats with different `customerChoices`.
|
|
114
|
+
2. **Prefer `addOnIds` over `durationInMinutes`** in the request — when both are present they must agree, so let Wix Bookings compute total duration from add-ons rather than passing a duration you'd have to keep in sync.
|
|
115
|
+
3. **`splitIntervalInMinutes` vs `duration`** — when the split interval matches the service duration, `splitIntervalInMinutes` is `duration.defaultInMinutes + bufferTimeInMinutes`.
|
|
116
|
+
4. **Duration ranges** — use `durationUnit` for services where the customer picks the length of the booking; leave `duration` empty for `DAY`-unit ranges.
|
|
117
|
+
5. **Called on the hot path** — this handler runs on every availability query, so keep it fast.
|
|
118
|
+
6. **`services` is optional on the request type** — default it to `[]` when destructuring, or the `.map()` calls fail `tsc` with "possibly undefined."
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Booking Policy Service Plugin Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Booking Policy SPI lets you provide dynamic booking policies instead of a service's single static policy. Wix calls your `listBookingPolicies` handler whenever it needs to know how early/late a service can be booked or whether online booking is allowed — for example, when a customer loads the booking calendar or books a service. Your implementation can return a different policy for the same service based on your own criteria (e.g., customer type, time of year).
|
|
6
|
+
|
|
7
|
+
## Request and Response Schema
|
|
8
|
+
|
|
9
|
+
Before implementing, call `ReadFullDocsMethodSchema` on the docs URL to get the full request/response types.
|
|
10
|
+
|
|
11
|
+
| Handler | Docs URL |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `listBookingPolicies` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/policies/booking-policy-service-plugin/list-booking-policies?apiView=SDK |
|
|
14
|
+
|
|
15
|
+
The request's `serviceIds` array has 1–8 service GUIDs. Your response's `bookingPolicies` array must contain exactly one policy per requested service, **in the same order** — all fields are required unless the schema marks them optional.
|
|
16
|
+
|
|
17
|
+
## Example: Different Early-Booking Windows by Service Category
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { bookingPolicy } from "@wix/bookings/service-plugins";
|
|
21
|
+
import { auth } from "@wix/essentials";
|
|
22
|
+
import { services } from "@wix/bookings";
|
|
23
|
+
|
|
24
|
+
bookingPolicy.provideHandlers({
|
|
25
|
+
listBookingPolicies: async (payload) => {
|
|
26
|
+
const { request } = payload;
|
|
27
|
+
const { serviceIds } = request;
|
|
28
|
+
|
|
29
|
+
// queryServices is a fluent query builder, not a request-object call — it returns { items }, not { services }.
|
|
30
|
+
const elevatedQueryServices = auth.elevate(services.queryServices);
|
|
31
|
+
const { items: matchedServices } = await elevatedQueryServices().in('_id', serviceIds).find();
|
|
32
|
+
const serviceById = new Map((matchedServices ?? []).map((s) => [s._id, s]));
|
|
33
|
+
|
|
34
|
+
const bookingPolicies = serviceIds.map((serviceId) => {
|
|
35
|
+
const isPremium = serviceById.get(serviceId)?.category?.name === "Premium";
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
serviceId,
|
|
39
|
+
limitEarlyBooking: {
|
|
40
|
+
enabled: true,
|
|
41
|
+
earliestBookingInMinutes: isPremium ? 20160 : 10080, // 14 vs 7 days
|
|
42
|
+
},
|
|
43
|
+
limitLateBooking: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
latestBookingInMinutes: 1440, // 1 day
|
|
46
|
+
},
|
|
47
|
+
onlineBooking: { enabled: true },
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return { bookingPolicies };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Manual Setup Required
|
|
57
|
+
|
|
58
|
+
None. Confirmed the overall availability/policy pipeline this plugin feeds into is live and enforcing real constraints (a dashboard booking attempt was correctly rejected for being outside staff working hours) — this SPI's own specific early/late-booking window differentiation wasn't isolated independently of that pipeline, but no dashboard configuration is needed beyond having the app installed and released.
|
|
59
|
+
|
|
60
|
+
## Key Implementation Notes
|
|
61
|
+
|
|
62
|
+
1. **One policy per service, same order** — the response array must line up positionally with the request's `serviceIds`; don't omit or reorder entries.
|
|
63
|
+
2. **All fields required unless marked optional** — return `limitEarlyBooking`, `limitLateBooking`, and `onlineBooking` fully populated rather than partially.
|
|
64
|
+
3. **Called on the hot path** — this handler runs whenever availability is queried or a booking is made, so keep it fast and avoid unnecessary external calls.
|
|
65
|
+
4. **Elevate permissions** — use `auth.elevate` when querying Wix APIs (e.g., service details) from the handler.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Bookings Pricing Provider Service Plugin Reference
|
|
2
|
+
|
|
3
|
+
## ⚠️ Requires manual sign-up before it's live
|
|
4
|
+
|
|
5
|
+
Unlike every other SPI in this skill, this integration isn't self-serve. Before Wix will ever call your handler on a real site, email `bookings-integration@wix.com` with subject "Bookings Pricing Integration" and include: your business type, a few words about your business/market, the URL of the site hosting your app, and your app ID. Wix reviews the request and replies by email. **Implement and build the plugin, but tell the user this sign-up step is required and still pending** — don't present the feature as live just because the code compiles and builds.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The Bookings Pricing Provider lets your app calculate custom prices for bookings — regional tax, discounts for specific customer types, or any combination — instead of Wix's standard per-variant pricing. Once installed, your plugin becomes the **sole source of price** for the service: Wix does not hand you a pre-computed subtotal to adjust, so if you need a base price, derive or store it yourself (e.g. read it from the service via `@wix/bookings`, or from your own config).
|
|
10
|
+
|
|
11
|
+
Wix calls your handler only *after* a booking already exists (`booking._id` is set) — this SPI cannot be used in advance to preview a price list. There is only one handler; there's no separate "preview" handler.
|
|
12
|
+
|
|
13
|
+
**FQDN**: `wix.bookings.pricing.v1.pricing_provider`
|
|
14
|
+
|
|
15
|
+
## Request and Response Schema
|
|
16
|
+
|
|
17
|
+
Before implementing, call `ReadFullDocsMethodSchema` on the docs URL to get the full request/response types. Note: the public docs page for this SPI still describes an older REST/webhook-only integration and does not yet reflect the `provideHandlers` SDK wrapper below — trust what `wix generate` actually scaffolds and the installed `@wix/auto_sdk_bookings_pricing-provider` types over the doc's prose/code samples, but its **field descriptions** (e.g. `booking.contactDetails.fullAddress.subdivision`) and the sign-up requirement above are still accurate.
|
|
18
|
+
|
|
19
|
+
| Handler | Docs URL |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `calculatePrice` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/pricing/pricing-integration-service-plugin/calculate-price |
|
|
22
|
+
|
|
23
|
+
Your handler receives `{ request: { booking }, metadata }`, where `booking` is the full [booking object](https://dev.wix.com/docs/rest/business-solutions/bookings/bookings/bookings-writer-v2/booking-object) — pull whatever you need from it (e.g. `booking.contactDetails.fullAddress.subdivision` for regional tax, `booking.totalParticipants` / `booking.participantsChoices` for participant count). Return **either**:
|
|
24
|
+
|
|
25
|
+
- `{ calculatedPrice: <number> }` — a numeric price. The site owner's dashboard can perform further calculations on top of it, but calling the Preview Price API on a booking priced this way fails.
|
|
26
|
+
- `{ priceDescription: <string> }` — a textual price description — lets Preview Price succeed, but Wix cannot do further numeric calculations on it.
|
|
27
|
+
|
|
28
|
+
Decide which shape to return before you write the handler; you can't mix both per request.
|
|
29
|
+
|
|
30
|
+
## Example: Regional Tax From Contact Address
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { pricingProvider } from "@wix/bookings/service-plugins";
|
|
34
|
+
|
|
35
|
+
const TAX_RATE_BY_SUBDIVISION: Record<string, number> = {
|
|
36
|
+
CA: 0.0725,
|
|
37
|
+
NY: 0.08,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const BASE_PRICE_PER_PARTICIPANT = 100; // wire this to your real per-service price
|
|
41
|
+
|
|
42
|
+
pricingProvider.provideHandlers({
|
|
43
|
+
calculatePrice: async (payload) => {
|
|
44
|
+
const { booking } = payload.request;
|
|
45
|
+
if (!booking) {
|
|
46
|
+
return { calculatedPrice: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const participants =
|
|
50
|
+
booking.totalParticipants ??
|
|
51
|
+
(booking.participantsChoices?.serviceChoices ?? []).reduce(
|
|
52
|
+
(sum, choice) => sum + (choice.numberOfParticipants ?? 0),
|
|
53
|
+
0
|
|
54
|
+
) ??
|
|
55
|
+
1;
|
|
56
|
+
|
|
57
|
+
const subdivision = booking.contactDetails?.fullAddress?.subdivision;
|
|
58
|
+
const taxRate = subdivision ? TAX_RATE_BY_SUBDIVISION[subdivision] ?? 0 : 0;
|
|
59
|
+
|
|
60
|
+
return { calculatedPrice: BASE_PRICE_PER_PARTICIPANT * participants * (1 + taxRate) };
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Key Implementation Notes
|
|
66
|
+
|
|
67
|
+
1. **Sign-up is mandatory and manual** — see the warning above; there is no self-serve activation flow.
|
|
68
|
+
2. **You own pricing entirely** — the request doesn't carry a merchant-configured base price; compute or look one up yourself.
|
|
69
|
+
3. **Only called after booking creation** — you cannot use this SPI to display prices in advance of a booking existing.
|
|
70
|
+
4. **Pick one response shape** — a numeric `calculatedPrice` (lets the dashboard add further calculations, but breaks Preview Price) or a `priceDescription` (the reverse). Decide once, per your app's use case.
|
|
71
|
+
5. **Confirm with the user before promising this feature** — since activation depends on an out-of-band email approval, tell the user this step is required rather than assuming the integration is live once code is deployed.
|
|
@@ -36,10 +36,12 @@ import { extendedBookings } from "@wix/bookings";
|
|
|
36
36
|
staffSorting.provideHandlers({
|
|
37
37
|
sortStaffMembers: async (payload) => {
|
|
38
38
|
const { request } = payload;
|
|
39
|
-
|
|
39
|
+
// availableResourceIds is optional on the request type — default it, or
|
|
40
|
+
// spreading/iterating it below fails `tsc` with "possibly undefined."
|
|
41
|
+
const { availableResourceIds = [], slot } = request;
|
|
40
42
|
|
|
41
43
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
42
|
-
const elevatedQuery = auth.elevate(extendedBookings.
|
|
44
|
+
const elevatedQuery = auth.elevate(extendedBookings.queryExtendedBookings);
|
|
43
45
|
const result = await elevatedQuery({
|
|
44
46
|
filter: {
|
|
45
47
|
"bookedEntity.item.slot.resource.id": { "$in": availableResourceIds },
|
|
@@ -73,10 +75,16 @@ staffSorting.provideHandlers({
|
|
|
73
75
|
});
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
## Manual Setup Required
|
|
79
|
+
|
|
80
|
+
None. Confirmed live with a service that has 2 assigned staff — the dashboard's "Add booking" flow resolved a specific staff member from the plugin's sorted order, and the booking created successfully with no errors. No dashboard configuration is needed beyond having the app installed and released, and the service having 2+ staff assigned to genuinely exercise the sort.
|
|
81
|
+
|
|
76
82
|
## Key Implementation Notes
|
|
77
83
|
|
|
78
84
|
1. **Return all IDs** - You must return every ID from `availableResourceIds`, just reordered
|
|
79
85
|
2. **Performance matters** - Keep logic fast; the booking flow waits for your response
|
|
80
86
|
3. **Elevate permissions** - Use `auth.elevate` when querying Wix APIs from the handler
|
|
81
87
|
4. **Deterministic sorting** - Use a tiebreaker (e.g., resource ID) when priorities are equal
|
|
82
|
-
5. **
|
|
88
|
+
5. **Use `queryExtendedBookings`, not `query`** — `extendedBookings.query` is deprecated. `queryExtendedBookings` takes the same `filter`/`cursorPaging` shape, so it's a drop-in replacement; confirmed live after switching.
|
|
89
|
+
6. **`availableResourceIds` is optional on the request type** — default it to `[]` when destructuring, or spreading/iterating it fails `tsc` with "possibly undefined."
|
|
90
|
+
7. **Graceful degradation** - If your external data source is unavailable, return the original order rather than failing
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Bookings Validation Service Plugin Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Bookings Validation SPI lets you implement custom validation logic for booking operations. When a customer creates, cancels, or reschedules a booking (single-service or multi-service), Wix calls the handler matching that operation before it executes. Return `valid: true` to allow the operation or `valid: false` to block it with a customer-facing message.
|
|
6
|
+
|
|
7
|
+
`bookingsValidation.provideHandlers` requires all six handlers in the object literal — TypeScript rejects a partial object even though you may only care about one or two targets. For the ones you don't have real logic for, provide a no-op that returns an empty `results`/`singleServiceBookingResults` array (an empty array is a no-op: "Omitting an item's result treats it as valid," and an empty results array omits every item's result). Confirmed live: `wix generate` itself scaffolds all six handlers by default — that's not incidental boilerplate you can trim.
|
|
8
|
+
|
|
9
|
+
## Handlers
|
|
10
|
+
|
|
11
|
+
| Handler | Validation target | On error/timeout |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `validateBeforeCreate` | `CREATE` | Blocked (fail-closed) |
|
|
14
|
+
| `validateBeforeCancel` | `CANCEL` | Blocked (fail-closed) |
|
|
15
|
+
| `validateBeforeReschedule` | `RESCHEDULE` | Continues (fail-open) |
|
|
16
|
+
| `validateBeforeCreateMultiService` | `CREATE_MULTI_SERVICE` | Blocked (fail-closed) |
|
|
17
|
+
| `validateBeforeCancelMultiService` | `CANCEL_MULTI_SERVICE` | Blocked (fail-closed) |
|
|
18
|
+
| `validateBeforeRescheduleMultiService` | `RESCHEDULE_MULTI_SERVICE` | Continues (fail-open) |
|
|
19
|
+
|
|
20
|
+
## Request and Response Schema
|
|
21
|
+
|
|
22
|
+
Before implementing, call `ReadFullDocsMethodSchema` on each docs URL to get the full request/response types.
|
|
23
|
+
|
|
24
|
+
| Handler | Docs URL |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `validateBeforeCreate` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-create?apiView=SDK |
|
|
27
|
+
| `validateBeforeCancel` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-cancel?apiView=SDK |
|
|
28
|
+
| `validateBeforeReschedule` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-reschedule?apiView=SDK |
|
|
29
|
+
| `validateBeforeCreateMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-create-multi-service?apiView=SDK |
|
|
30
|
+
| `validateBeforeCancelMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-cancel-multi-service?apiView=SDK |
|
|
31
|
+
| `validateBeforeRescheduleMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-reschedule-multi-service?apiView=SDK |
|
|
32
|
+
|
|
33
|
+
**Correlation differs by handler — this is easy to get wrong, and doing so fails `tsc`, not just at runtime:**
|
|
34
|
+
|
|
35
|
+
| Handler(s) | Item shape | Max items | Result correlates by |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `validateBeforeCreate`, `validateBeforeCreateMultiService` | `{ itemIndex, booking }` | 40 | `itemIndex` |
|
|
38
|
+
| `validateBeforeCancel`, `validateBeforeReschedule`, and their multi-service equivalents | `{ booking }` — **no `itemIndex` field at all** | 8 | `bookingId` (from `booking._id`) |
|
|
39
|
+
|
|
40
|
+
Only the create family carries `itemIndex`; cancel and reschedule items don't have one, so their results key on the booking's own ID instead. The single-service response field is `results`; the multi-service response field is `singleServiceBookingResults` — same per-item shape either way. Omitting an item's result treats it as valid. `contactDetails` and resource `name`/`email` fields are redacted before reaching your handler.
|
|
41
|
+
|
|
42
|
+
## Example: Membership Limit and Cancellation Fee
|
|
43
|
+
|
|
44
|
+
This example enforces a pricing-plan booking limit on create, and blocks late cancellations.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { bookingsValidation } from "@wix/bookings/service-plugins";
|
|
48
|
+
import { auth } from "@wix/essentials";
|
|
49
|
+
import { orders } from "@wix/pricing-plans";
|
|
50
|
+
|
|
51
|
+
bookingsValidation.provideHandlers({
|
|
52
|
+
validateBeforeCreate: async (payload) => {
|
|
53
|
+
const { request } = payload;
|
|
54
|
+
|
|
55
|
+
const results = await Promise.all(
|
|
56
|
+
(request.items ?? []).map(async (item) => {
|
|
57
|
+
const memberId = item.booking?.contactDetails?.contactId;
|
|
58
|
+
if (!memberId) {
|
|
59
|
+
return { itemIndex: item.itemIndex, result: { valid: true } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Not orders.listOrders — that method doesn't exist on @wix/pricing-plans.
|
|
63
|
+
// managementListOrders takes flat buyerIds/orderStatuses, not a nested filter.
|
|
64
|
+
const elevatedListOrders = auth.elevate(orders.managementListOrders);
|
|
65
|
+
const { orders: activeOrders } = await elevatedListOrders({
|
|
66
|
+
buyerIds: [memberId],
|
|
67
|
+
orderStatuses: ["ACTIVE"],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if ((activeOrders ?? []).length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
itemIndex: item.itemIndex,
|
|
73
|
+
result: {
|
|
74
|
+
valid: false,
|
|
75
|
+
invalidReason: {
|
|
76
|
+
message: "An active membership is required to book this service.",
|
|
77
|
+
fieldViolations: [
|
|
78
|
+
{ field: "booking.contactDetails", description: "No active membership found.", code: "PLAN_EXPIRED" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { itemIndex: item.itemIndex, result: { valid: true } };
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return { results };
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
validateBeforeCancel: async (payload) => {
|
|
93
|
+
const { request } = payload;
|
|
94
|
+
|
|
95
|
+
// Cancel items carry no itemIndex — correlate results by the booking's own _id.
|
|
96
|
+
const results = (request.items ?? []).map((item) => {
|
|
97
|
+
const bookingId = item.booking?._id ?? undefined;
|
|
98
|
+
const startDate = item.booking?.bookedEntity?.slot?.startDate;
|
|
99
|
+
const hoursUntilStart = startDate
|
|
100
|
+
? (new Date(startDate).getTime() - Date.now()) / (1000 * 60 * 60)
|
|
101
|
+
: Infinity;
|
|
102
|
+
|
|
103
|
+
if (hoursUntilStart < 24) {
|
|
104
|
+
return {
|
|
105
|
+
bookingId,
|
|
106
|
+
result: {
|
|
107
|
+
valid: false,
|
|
108
|
+
invalidReason: { message: "Cancellations within 24 hours of the appointment aren't allowed." },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { bookingId, result: { valid: true } };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { results };
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Unimplemented targets still need a handler — see the note above the example.
|
|
120
|
+
validateBeforeReschedule: async () => ({ results: [] }),
|
|
121
|
+
validateBeforeCreateMultiService: async () => ({ singleServiceBookingResults: [] }),
|
|
122
|
+
validateBeforeCancelMultiService: async () => ({ singleServiceBookingResults: [] }),
|
|
123
|
+
validateBeforeRescheduleMultiService: async () => ({ singleServiceBookingResults: [] }),
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Manual Setup Required
|
|
128
|
+
|
|
129
|
+
None — confirmed live via the real Cancel Booking API: a cancellation under 24h was correctly blocked with a structured `428 VALIDATION_FAILED`, and one over 24h correctly succeeded. Wix calls this plugin automatically on the relevant booking operations once the app is installed and released.
|
|
130
|
+
|
|
131
|
+
## Key Implementation Notes
|
|
132
|
+
|
|
133
|
+
1. **Bulk requests, per-item results** — return a result for every item you received (keyed by `itemIndex` on create, by `bookingId` on cancel/reschedule — see the correlation table above); a missing entry is treated as valid, not rejected.
|
|
134
|
+
2. **Respond fast** — `validateBeforeCreate`, `validateBeforeCancel`, and their multi-service equivalents fail-closed (block the operation) on error or timeout; `validateBeforeReschedule` and its multi-service equivalent fail-open (allow it).
|
|
135
|
+
3. **Multiple providers run concurrently** — if other apps also register validators for the same target, any single rejection blocks the operation.
|
|
136
|
+
4. **Write customer-facing messages** — `invalidReason.message` and `fieldViolations[].description` are shown to the customer; keep them clear and free of internal jargon. `fieldViolations[].code` is for your own programmatic handling, not display.
|
|
137
|
+
5. **Elevate permissions** — use `auth.elevate` when querying Wix APIs (e.g., pricing plan orders) from the handler.
|
|
@@ -20,6 +20,10 @@ Before implementing, call `ReadFullDocsMethodSchema` on each docs URL to get the
|
|
|
20
20
|
| `getEligibleTriggers` | https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/custom-discount-triggers-integration-service-plugin/get-eligible-triggers?apiView=SDK |
|
|
21
21
|
| `listTriggers` | https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/custom-discount-triggers-integration-service-plugin/list-triggers?apiView=SDK |
|
|
22
22
|
|
|
23
|
+
## ⚠️ `identifier` must be echoed from the request, never invented
|
|
24
|
+
|
|
25
|
+
`getEligibleTriggers` receives `request.triggers[]`, each with a `customTrigger._id` and an `identifier` Wix generated for that occurrence. Your response's `eligibleTriggers[i].identifier` **must be the exact same value** from the matching `request.triggers[i].identifier` — it's how Wix correlates your answer back to the discount rule that asked. A hardcoded or self-invented `identifier` never matches, so the discount silently never applies — confirmed live: the discount rule stays wired up and no error is raised, it just never fires.
|
|
26
|
+
|
|
23
27
|
## Example: Happy Hour and Digital Products Triggers
|
|
24
28
|
|
|
25
29
|
This example defines two custom triggers: a time-based "Happy Hour" trigger and a product-type-based "Digital Sale" trigger.
|
|
@@ -30,22 +34,16 @@ import { customTriggers } from "@wix/ecom/service-plugins";
|
|
|
30
34
|
customTriggers.provideHandlers({
|
|
31
35
|
getEligibleTriggers: async (payload) => {
|
|
32
36
|
const { request, metadata } = payload;
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
customTriggerId: "my-digital-sale-trigger",
|
|
45
|
-
identifier: "234",
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
};
|
|
37
|
+
// request.triggers tells you which trigger occurrences Wix is asking about —
|
|
38
|
+
// filter to the ones you recognize and echo back their `identifier` exactly.
|
|
39
|
+
const eligibleTriggers = (request.triggers ?? [])
|
|
40
|
+
.filter((t) => t.customTrigger?._id === "my-happy-hour-trigger" /* + your eligibility check */)
|
|
41
|
+
.map((t) => ({
|
|
42
|
+
customTriggerId: "my-happy-hour-trigger",
|
|
43
|
+
identifier: t.identifier,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return { eligibleTriggers };
|
|
49
47
|
},
|
|
50
48
|
listTriggers: async (payload) => {
|
|
51
49
|
const { request, metadata } = payload;
|
|
@@ -79,3 +77,12 @@ customTriggers.provideHandlers({
|
|
|
79
77
|
2. **Both handlers required** - You must implement both `getEligibleTriggers` and `listTriggers`
|
|
80
78
|
3. **Dynamic eligibility** - `getEligibleTriggers` is called during checkout to determine which triggers are currently active
|
|
81
79
|
4. **Static list** - `listTriggers` provides the master list of all possible triggers for configuration in the Wix dashboard
|
|
80
|
+
5. **Manual step required, every time** — this plugin only supplies *eligibility*. It has no visible effect until the merchant creates an Automatic Discount that uses your trigger. Tell the user this step is required; don't imply the discount is live just because the plugin is installed. Confirmed live, click-by-click in the dashboard:
|
|
81
|
+
1. Go to **Catalog** (left sidebar) → **Discounts** → **Automatic Discounts**.
|
|
82
|
+
2. Click **Create Discount**.
|
|
83
|
+
3. Pick a discount type (e.g. **Standard discount**) and click **Continue**.
|
|
84
|
+
4. Under **Offer** → **"Are there any requirements?"**, check **Custom requirement**.
|
|
85
|
+
5. A dropdown appears next to it — click it and select your trigger by the `name` your `listTriggers` handler returned for it (e.g. "Happy Hour 16:00-18:00"). It only shows up here once your app is installed and `listTriggers` includes it — there's no dashboard flow to type in an arbitrary trigger ID.
|
|
86
|
+
6. Fill in the discount's offer/eligibility/dates as normal, then click **Save**.
|
|
87
|
+
|
|
88
|
+
Note: the dialog's own **"Create New"** button under that dropdown only offers Wix's *built-in* requirement templates (email domain, first purchase, order count, etc.) — it does not create a new app-linked custom trigger. Your trigger must already be coming back from `listTriggers` to appear in the dropdown at all.
|
|
@@ -67,6 +67,10 @@ giftVouchersProvider.provideHandlers({
|
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Manual Setup Required
|
|
71
|
+
|
|
72
|
+
No dashboard configuration beyond installing the app. But there's nothing to `redeem`/`getBalance` against until a real gift card exists — a customer (or you, via the Wix Gift Cards app's own purchase/issuance flow) must actually buy or be issued a gift card first. You can't shortcut this by calling `redeem` with a made-up code; the code has to correspond to a gift card your provider recognizes as real. Test by issuing a real gift card through the site's own gift-card purchase flow, then redeeming it at checkout.
|
|
73
|
+
|
|
70
74
|
## Singular Constraint
|
|
71
75
|
|
|
72
76
|
`GIFT_CARDS_PROVIDER` is **singular** — only one component of this type is allowed per app. Do not scaffold or include two Gift Cards service plugins in the same app.
|
|
@@ -45,7 +45,6 @@ paymentSettings.provideHandlers({
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
|
-
getPaymentSettingsForCheckout: async () => ({ blockedPaymentOptions: [] }),
|
|
49
48
|
});
|
|
50
49
|
```
|
|
51
50
|
|
|
@@ -84,10 +83,13 @@ paymentSettings.provideHandlers({
|
|
|
84
83
|
};
|
|
85
84
|
}
|
|
86
85
|
},
|
|
87
|
-
getPaymentSettingsForCheckout: async () => ({ blockedPaymentOptions: [] }),
|
|
88
86
|
});
|
|
89
87
|
```
|
|
90
88
|
|
|
89
|
+
## Manual Setup Required
|
|
90
|
+
|
|
91
|
+
No dashboard configuration beyond installing the app — Wix calls this SPI automatically once installed. The one real prerequisite: **a payment provider must already be connected** on the site (Wix Payments or another provider), or there's nothing for `requires3dSecure` to apply to. You can't exercise this handler with just a Cart `calculate`/`refresh` call — it only fires during actual payment submission (a customer entering card details), so verifying it end-to-end needs a real or sandbox payment attempt, not just an API-level cart inspection.
|
|
92
|
+
|
|
91
93
|
## Key Implementation Notes
|
|
92
94
|
|
|
93
95
|
1. **Payment provider required** - Site must have a payment provider connected to collect payments
|