@riverbankcms/sdk 0.74.0 → 0.76.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 (145) hide show
  1. package/README.md +2 -0
  2. package/dist/{PreviewEditorSidebar-5XL7M3OM.mjs → PreviewEditorSidebar-X2HWXFOL.mjs} +172 -89
  3. package/dist/{PreviewEditorUI-ZDFRDJID.mjs → PreviewEditorUI-AQXIXPOH.mjs} +3 -3
  4. package/dist/SdkPreviewModeRuntime-ZQNDCASH.mjs +8 -0
  5. package/dist/_dts/api/src/bookingOperations.d.ts +12 -0
  6. package/dist/_dts/api/src/cache-tags.d.ts +22 -0
  7. package/dist/_dts/api/src/contentRuntime.d.ts +1 -0
  8. package/dist/_dts/api/src/endpoints.d.ts +24 -24
  9. package/dist/_dts/api/src/index.d.ts +1 -1
  10. package/dist/_dts/api/src/publicExperienceEndpoints.d.ts +13 -4
  11. package/dist/_dts/block-form/src/blockDraftSaveability.d.ts +4 -2
  12. package/dist/_dts/block-form/src/client/index.d.ts +1 -1
  13. package/dist/_dts/block-form/src/manifest/utils.d.ts +24 -0
  14. package/dist/_dts/blocks/src/client/booking-form.d.ts +2 -0
  15. package/dist/_dts/blocks/src/client/carousel.d.ts +1 -0
  16. package/dist/_dts/blocks/src/client/course-registration.d.ts +2 -0
  17. package/dist/_dts/blocks/src/client/events.d.ts +6 -0
  18. package/dist/_dts/blocks/src/client/form.d.ts +2 -0
  19. package/dist/_dts/blocks/src/client/header.d.ts +1 -0
  20. package/dist/_dts/blocks/src/client/location-map.d.ts +1 -0
  21. package/dist/_dts/blocks/src/client/newsletter-form.d.ts +2 -0
  22. package/dist/_dts/blocks/src/client/shop.d.ts +6 -0
  23. package/dist/_dts/blocks/src/contracts/proof-block-islands.d.ts +4 -4
  24. package/dist/_dts/blocks/src/lib/video.d.ts +2 -0
  25. package/dist/_dts/blocks/src/system/manifest/fieldValidation/index.d.ts +21 -1
  26. package/dist/_dts/blocks/src/system/runtime/components/registry.client.d.ts +7 -6
  27. package/dist/_dts/blocks/src/system/runtime/components/runtime-backed-blocks.d.ts +3 -3
  28. package/dist/_dts/blocks/src/system/runtime/nodes/booking-form.headless.state.d.ts +3 -2
  29. package/dist/_dts/blocks/src/system/runtime/nodes/booking-form.interactive.d.ts +3 -27
  30. package/dist/_dts/blocks/src/system/runtime/nodes/booking-runtime-sources.d.ts +163 -0
  31. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration.interactive.d.ts +5 -5
  32. package/dist/_dts/blocks/src/system/runtime/nodes/events/index.d.ts +3 -0
  33. package/dist/_dts/blocks/src/system/runtime/nodes/header-section.client.d.ts +1 -0
  34. package/dist/_dts/blocks/src/system/runtime/nodes/header-section.server.d.ts +1 -0
  35. package/dist/_dts/blocks/src/system/runtime/shared/components/BookingOptionCompactRow.d.ts +29 -0
  36. package/dist/_dts/blocks/src/system/runtime/shared/components/BookingOptionList.d.ts +13 -0
  37. package/dist/_dts/blocks/src/system/runtime/shared/components/index.d.ts +4 -0
  38. package/dist/_dts/blocks/src/system/runtime/shared/index.d.ts +4 -3
  39. package/dist/_dts/blocks/src/system/runtime/shared/utils/bookingRoute.d.ts +20 -0
  40. package/dist/_dts/blocks/src/system/runtime/shared/utils/index.d.ts +2 -1
  41. package/dist/_dts/blocks/src/system/types/link.d.ts +12 -0
  42. package/dist/_dts/content-editor/src/hooks/validateBlocksForPublish.d.ts +5 -0
  43. package/dist/_dts/editor-blocks/src/BlockEditor.d.ts +2 -0
  44. package/dist/_dts/editor-blocks/src/block-editor/BlockCard.d.ts +3 -1
  45. package/dist/_dts/editor-blocks/src/block-editor/BlockCardHeader.d.ts +3 -1
  46. package/dist/_dts/editor-blocks/src/block-editor/BlockSettingsModal.d.ts +5 -1
  47. package/dist/_dts/editor-blocks/src/block-editor/SortableBlockItem.d.ts +2 -0
  48. package/dist/_dts/editor-blocks/src/block-editor/actionControlMode.d.ts +2 -0
  49. package/dist/_dts/editor-blocks/src/index.d.ts +1 -1
  50. package/dist/_dts/preview-next/src/client/preview/SiteChromeEditableSurface.d.ts +12 -2
  51. package/dist/_dts/preview-next/src/client/preview/themeOverlay.d.ts +21 -3
  52. package/dist/_dts/sdk/src/components.d.ts +2 -2
  53. package/dist/_dts/sdk/src/public-api/contracts.d.ts +1 -0
  54. package/dist/_dts/sdk/src/rendering/index.d.ts +2 -2
  55. package/dist/_dts/sdk/src/rendering/islands/deferredVideoEmbeds.d.ts +2 -0
  56. package/dist/_dts/sdk/src/rendering/overrideResolution.d.ts +6 -0
  57. package/dist/_dts/sdk/src/rendering/overrides.d.ts +21 -1
  58. package/dist/_dts/sdk/src/rendering/server.d.ts +4 -0
  59. package/dist/_dts/sdk/src/version.d.ts +1 -1
  60. package/dist/_dts/theme-core/src/generated/containerResponsiveThemeCss.d.ts +1 -1
  61. package/dist/{chunk-2MIWNFJM.mjs → chunk-LYI7TEMH.mjs} +494 -171
  62. package/dist/{chunk-YY7K3QD2.mjs → chunk-Q4OLG66B.mjs} +9 -9
  63. package/dist/{chunk-CEQBBLMJ.mjs → chunk-TW2Y4RR5.mjs} +69 -14
  64. package/dist/{chunk-CBD47MTM.mjs → chunk-V5VORNEJ.mjs} +2 -2
  65. package/dist/{chunk-7RX72SK2.mjs → chunk-VX7B5VAC.mjs} +36 -25
  66. package/dist/{chunk-YRJKZEQ3.mjs → chunk-Z7JLNTJR.mjs} +1 -1
  67. package/dist/cli/index.mjs +3395 -3753
  68. package/dist/client/{accordion-ZZVXWV3N.mjs → accordion-2YHIBOZH.mjs} +1 -1
  69. package/dist/client/bookingForm-35IBDWAS.mjs +3012 -0
  70. package/dist/client/bookings.mjs +30 -21
  71. package/dist/client/{carousel-25WK2H7G.mjs → carousel-K6XG7NZQ.mjs} +3 -2
  72. package/dist/client/cart-HCFYUTTU.mjs +18 -0
  73. package/dist/client/checkout-J3U4XR7K.mjs +18 -0
  74. package/dist/client/chunk-3DCJ45Z2.mjs +36 -0
  75. package/dist/client/chunk-3PEDD5KJ.mjs +146 -0
  76. package/dist/client/{chunk-6VGXRE34.mjs → chunk-7UOSCYY4.mjs} +76 -29
  77. package/dist/client/chunk-BOIHIU7E.mjs +106 -0
  78. package/dist/client/chunk-F7LHOKEH.mjs +146 -0
  79. package/dist/client/chunk-FGUIVT2B.mjs +204 -0
  80. package/dist/client/chunk-I2MMZO53.mjs +6 -0
  81. package/dist/client/chunk-II7O4XN3.mjs +2206 -0
  82. package/dist/client/chunk-J227A3CQ.mjs +245 -0
  83. package/dist/client/chunk-JEZECJ3W.mjs +383 -0
  84. package/dist/client/chunk-M3L3KYYR.mjs +1181 -0
  85. package/dist/client/chunk-OJPZEZU2.mjs +31 -0
  86. package/dist/client/chunk-PFBX3IQL.mjs +1378 -0
  87. package/dist/client/chunk-RTZ3JKKX.mjs +178 -0
  88. package/dist/client/chunk-SNSWVZEP.mjs +5592 -0
  89. package/dist/client/chunk-TQ5MSPDX.mjs +74 -0
  90. package/dist/client/chunk-YLMKMCSJ.mjs +86 -0
  91. package/dist/client/client.mjs +54052 -54518
  92. package/dist/client/courseRegistration-ZBXS7VGI.mjs +1737 -0
  93. package/dist/client/{eventCalendar-X6GIPYFM.mjs → eventCalendar-EQQZHJBA.mjs} +13 -2
  94. package/dist/client/{eventCombined-EZUOPM3G.mjs → eventCombined-JCFTGBFQ.mjs} +13 -2
  95. package/dist/client/eventDetails-HLUTQSYY.mjs +25 -0
  96. package/dist/client/{eventListing-HEFGAETC.mjs → eventListing-FG4MICVB.mjs} +13 -2
  97. package/dist/client/{eventRegistration-3LCP2V3P.mjs → eventRegistration-GUEB7LBT.mjs} +13 -2
  98. package/dist/client/form-YQEW4YIT.mjs +300 -0
  99. package/dist/client/headerSection-GTR3TTRM.mjs +729 -0
  100. package/dist/client/hooks.mjs +49 -33
  101. package/dist/client/locationMap-SPC7VGUH.mjs +227 -0
  102. package/dist/client/newsletterForm-4IW5BLID.mjs +208 -0
  103. package/dist/client/productDetail-KP5H2WJO.mjs +18 -0
  104. package/dist/client/productList-3M3M72XZ.mjs +18 -0
  105. package/dist/client/rendering/client.mjs +15580 -16277
  106. package/dist/client/rendering/islands.mjs +98 -21
  107. package/dist/client/rendering.mjs +48730 -49191
  108. package/dist/client/shop-ACQKVGOM.mjs +20 -0
  109. package/dist/preview-next/before-render.mjs +37 -25
  110. package/dist/preview-next/chrome.css +1 -1
  111. package/dist/preview-next/client/runtime.mjs +5 -5
  112. package/dist/preview-next/editor-base.css +2 -148
  113. package/dist/preview-next/editor-chrome.css +179 -0
  114. package/dist/preview-next/middleware.mjs +37 -25
  115. package/dist/{sdk-runtime-OHNONZ5U.mjs → sdk-runtime-K7HNB4U2.mjs} +6 -6
  116. package/dist/server/components.mjs +1294 -1103
  117. package/dist/server/config-validation.mjs +49 -33
  118. package/dist/server/config.mjs +49 -33
  119. package/dist/server/data.mjs +49 -33
  120. package/dist/server/index.mjs +45 -33
  121. package/dist/server/next.mjs +1240 -1050
  122. package/dist/server/page-converter.mjs +12 -8
  123. package/dist/server/prebuild.mjs +1 -1
  124. package/dist/server/rendering/server.mjs +1389 -1104
  125. package/dist/server/rendering.mjs +1300 -1109
  126. package/dist/server/routing.mjs +49 -33
  127. package/dist/server/server.mjs +50 -34
  128. package/dist/server/theme-bridge.mjs +5 -0
  129. package/dist/styles/index.css +5 -0
  130. package/package.json +2 -1
  131. package/dist/SdkPreviewModeRuntime-CRYNDSPL.mjs +0 -8
  132. package/dist/client/bookingForm-4X77HKDS.mjs +0 -34
  133. package/dist/client/cart-JWD2VRFU.mjs +0 -11
  134. package/dist/client/checkout-V7KYGWL7.mjs +0 -11
  135. package/dist/client/chunk-5IYGMTUE.mjs +0 -18614
  136. package/dist/client/chunk-RKCFVIMS.mjs +0 -26
  137. package/dist/client/courseRegistration-O6ZPK4YT.mjs +0 -29
  138. package/dist/client/eventDetails-3WODIXET.mjs +0 -14
  139. package/dist/client/form-AKXQMY6Z.mjs +0 -85
  140. package/dist/client/headerSection-5KHCMU6B.mjs +0 -12
  141. package/dist/client/locationMap-7HIADW76.mjs +0 -14
  142. package/dist/client/newsletterForm-5N4YTMU3.mjs +0 -60
  143. package/dist/client/productDetail-IZUA7ZP3.mjs +0 -11
  144. package/dist/client/productList-KWIIJE6I.mjs +0 -11
  145. package/dist/client/shop-XZQFRQWZ.mjs +0 -13
@@ -0,0 +1,3012 @@
1
+ "use client";
2
+ import { DiscountCodeSection } from './chunk-OJPZEZU2.mjs';
3
+ import { useDiscountCodeUi, usePolling } from './chunk-J227A3CQ.mjs';
4
+ import { SpinnerNode } from './chunk-TQ5MSPDX.mjs';
5
+ import { scrollToBlock, buildCheckoutReturnUrls, formatPrice } from './chunk-JEZECJ3W.mjs';
6
+ import { FieldValidationMessage, getFieldValidationControlProps } from './chunk-3DCJ45Z2.mjs';
7
+ import './chunk-3PEDD5KJ.mjs';
8
+ import { isSpamProtectionEnabled, getChallengeUrl, AltchaWidget } from './chunk-YLMKMCSJ.mjs';
9
+ import { BlockApiProvider, useBlockApi, FeedbackMessage } from './chunk-M3L3KYYR.mjs';
10
+ import { mountReactIsland } from './chunk-N7PBWBWQ.mjs';
11
+ import { defineIslandPropsDecoder, resolveBookingSiteId, resolveDefaultBookingServiceId, getBookingRedirectStateFromLocation, deriveBookingFormViewState, logger, getBookingSuccessMessage, resolveAppointmentCheckoutVerificationState, themeButtonClassName, toSelectedCollectionOptionKind, assertNever, prepareBookingSubmission, formatBookingSubmissionPreparationError, resolveBookingPaymentStep, BOOKING_FORM_DATA_RESERVED_KEYS, okWith, err } from './chunk-7UOSCYY4.mjs';
12
+ import { resolveBookingTimeZoneId, DEFAULT_BOOKING_TIME_ZONE, asFormId, asAppointmentResourceId, asCustomerPassId, asAppointmentServiceId, formatLocalScheduleDate, addLocalScheduleDays, parseBookingLocalDate, resolveFlexibleBalanceInitialPaymentCents, resolveFlexibleBalanceDueAt, getBookingInstalmentDepositRefundability, asPassId, splitAmountWithRemainderInFirst, addIntervalPreservingMonthAnchor, clampConfiguredAmount, getBookingInstalmentPlanIntervalCount, omitZeroAmountFutureRows, formatBookingInstant } from './chunk-PMAIXMAB.mjs';
13
+ import React2, { useRef, useState, useMemo, useReducer, useEffect, useCallback, useLayoutEffect } from 'react';
14
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
+
16
+ var SuccessMessage = ({
17
+ message,
18
+ resourceName,
19
+ className
20
+ }) => {
21
+ return /* @__PURE__ */ jsx(
22
+ FeedbackMessage,
23
+ {
24
+ tone: "success",
25
+ className: `ab-alert ab-alert--success ${className ?? ""}`,
26
+ role: "alert",
27
+ live: "polite",
28
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ /* @__PURE__ */ jsx("p", { className: "ab-text", children: message }),
30
+ resourceName ? /* @__PURE__ */ jsxs("p", { className: "ab-text ab-text--muted", children: [
31
+ "Practitioner: ",
32
+ resourceName
33
+ ] }) : null
34
+ ] })
35
+ }
36
+ );
37
+ };
38
+
39
+ // ../blocks/src/utils/date-formatting.ts
40
+ var BOOKING_FETCH_CHUNK_DAYS = 14;
41
+ var hasWarnedAboutRuntimeTimeZoneFallback = false;
42
+ function warnRuntimeTimeZoneFallback() {
43
+ if (hasWarnedAboutRuntimeTimeZoneFallback) {
44
+ return;
45
+ }
46
+ hasWarnedAboutRuntimeTimeZoneFallback = true;
47
+ console.warn(
48
+ "[booking] Falling back to UTC because the browser timezone could not be resolved safely."
49
+ );
50
+ }
51
+ function getRuntimeTimeZone() {
52
+ try {
53
+ const resolvedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
54
+ const timeZone = resolveBookingTimeZoneId(resolvedTimeZone);
55
+ if (!resolvedTimeZone || timeZone !== resolvedTimeZone.trim()) {
56
+ warnRuntimeTimeZoneFallback();
57
+ }
58
+ return timeZone;
59
+ } catch {
60
+ warnRuntimeTimeZoneFallback();
61
+ return DEFAULT_BOOKING_TIME_ZONE;
62
+ }
63
+ }
64
+ function formatDate(dateStr) {
65
+ return formatBookingInstant(`${dateStr}T00:00:00.000Z`, {
66
+ timeZone: DEFAULT_BOOKING_TIME_ZONE,
67
+ format: {
68
+ weekday: "long",
69
+ year: "numeric",
70
+ month: "long",
71
+ day: "numeric"
72
+ }
73
+ });
74
+ }
75
+ function formatTime(isoString) {
76
+ return formatBookingInstant(isoString, {
77
+ timeZone: DEFAULT_BOOKING_TIME_ZONE,
78
+ format: {
79
+ hour: "numeric",
80
+ minute: "2-digit",
81
+ timeZoneName: "short"
82
+ }
83
+ });
84
+ }
85
+ var MIN_AVAILABILITY_REFRESH_INTERVAL_MS = 15e3;
86
+ function useVisibleTabPolling({
87
+ enabled,
88
+ intervalMs,
89
+ jitterMs,
90
+ minVisibleRefreshIntervalMs = MIN_AVAILABILITY_REFRESH_INTERVAL_MS,
91
+ onPoll
92
+ }) {
93
+ const onPollRef = useRef(onPoll);
94
+ const inFlightRef = useRef(false);
95
+ const lastPollStartedAtRef = useRef(0);
96
+ onPollRef.current = onPoll;
97
+ useEffect(() => {
98
+ if (!enabled) return;
99
+ if (typeof window === "undefined" || typeof document === "undefined")
100
+ return;
101
+ let timeoutId = null;
102
+ let cancelled = false;
103
+ const runPoll = async (reason) => {
104
+ if (cancelled || document.visibilityState === "hidden" || inFlightRef.current) {
105
+ return;
106
+ }
107
+ const now = Date.now();
108
+ if (reason === "visible" && now - lastPollStartedAtRef.current < minVisibleRefreshIntervalMs) {
109
+ return;
110
+ }
111
+ lastPollStartedAtRef.current = now;
112
+ inFlightRef.current = true;
113
+ try {
114
+ await onPollRef.current();
115
+ } finally {
116
+ inFlightRef.current = false;
117
+ }
118
+ };
119
+ const schedule = () => {
120
+ const jitter = Math.floor(Math.random() * jitterMs);
121
+ timeoutId = window.setTimeout(() => {
122
+ void runPoll("timer").finally(() => {
123
+ if (!cancelled) schedule();
124
+ });
125
+ }, intervalMs + jitter);
126
+ };
127
+ const handleVisibilityChange = () => {
128
+ if (document.visibilityState === "visible") {
129
+ void runPoll("visible");
130
+ }
131
+ };
132
+ lastPollStartedAtRef.current = Date.now();
133
+ schedule();
134
+ document.addEventListener("visibilitychange", handleVisibilityChange);
135
+ return () => {
136
+ cancelled = true;
137
+ if (timeoutId) window.clearTimeout(timeoutId);
138
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
139
+ };
140
+ }, [enabled, intervalMs, jitterMs, minVisibleRefreshIntervalMs]);
141
+ }
142
+
143
+ // ../blocks/src/system/runtime/hooks/useAvailableDates.ts
144
+ var AVAILABILITY_REFRESH_INTERVAL_MS = 3e4;
145
+ var AVAILABILITY_REFRESH_JITTER_MS = 5e3;
146
+ function useAvailableDates({
147
+ siteId: _siteId,
148
+ // siteId now comes from BlockApiProvider context
149
+ serviceId,
150
+ resourceId,
151
+ timezone,
152
+ initialDays = 30
153
+ }) {
154
+ const api = useBlockApi();
155
+ const [availableDates, setAvailableDates] = useState(/* @__PURE__ */ new Set());
156
+ const [isLoading, setIsLoading] = useState(false);
157
+ const [isRefreshing, setIsRefreshing] = useState(false);
158
+ const [error, setError] = useState(null);
159
+ const [hasMore, setHasMore] = useState(false);
160
+ const [loadedRange, setLoadedRange] = useState(null);
161
+ const nextStartDateRef = useRef(null);
162
+ const requestIdRef = useRef(0);
163
+ const fetchDates = useCallback(
164
+ async (startDate, endDate, append = false, mode = "load") => {
165
+ if (!serviceId) return;
166
+ const requestId = ++requestIdRef.current;
167
+ if (mode === "refresh") {
168
+ setIsRefreshing(true);
169
+ } else {
170
+ setIsLoading(true);
171
+ setIsRefreshing(false);
172
+ }
173
+ setError(null);
174
+ try {
175
+ const data = await api.getAvailableDates({
176
+ serviceId,
177
+ startDate,
178
+ endDate,
179
+ timezone,
180
+ resourceId
181
+ });
182
+ if (requestId !== requestIdRef.current) return;
183
+ setAvailableDates((prev) => {
184
+ const newSet = append ? new Set(prev) : /* @__PURE__ */ new Set();
185
+ data.dates.forEach((d) => newSet.add(d));
186
+ return newSet;
187
+ });
188
+ setHasMore(data.hasMore);
189
+ nextStartDateRef.current = data.nextStartDate || null;
190
+ setLoadedRange((prev) => ({
191
+ start: append && prev ? prev.start : data.startDate,
192
+ end: data.endDate
193
+ }));
194
+ } catch (err2) {
195
+ if (requestId !== requestIdRef.current) return;
196
+ setError(
197
+ err2 instanceof Error ? err2.message : "Failed to load available dates"
198
+ );
199
+ } finally {
200
+ if (mode === "refresh" && requestId === requestIdRef.current) {
201
+ setIsRefreshing(false);
202
+ } else if (requestId === requestIdRef.current) {
203
+ setIsLoading(false);
204
+ }
205
+ }
206
+ },
207
+ [api, serviceId, resourceId, timezone]
208
+ );
209
+ useEffect(() => {
210
+ if (!serviceId) {
211
+ setAvailableDates(/* @__PURE__ */ new Set());
212
+ setLoadedRange(null);
213
+ setHasMore(false);
214
+ setIsRefreshing(false);
215
+ return;
216
+ }
217
+ const now = /* @__PURE__ */ new Date();
218
+ const today = formatLocalScheduleDate({
219
+ year: now.getFullYear(),
220
+ month: now.getMonth() + 1,
221
+ day: now.getDate()
222
+ });
223
+ const endDate = formatLocalScheduleDate(
224
+ addLocalScheduleDays(parseBookingLocalDate(today), initialDays)
225
+ );
226
+ fetchDates(today, endDate, false);
227
+ }, [serviceId, fetchDates, initialDays]);
228
+ useVisibleTabPolling({
229
+ enabled: Boolean(serviceId && loadedRange),
230
+ intervalMs: AVAILABILITY_REFRESH_INTERVAL_MS,
231
+ jitterMs: AVAILABILITY_REFRESH_JITTER_MS,
232
+ onPoll: () => {
233
+ if (!loadedRange) return;
234
+ return fetchDates(loadedRange.start, loadedRange.end, false, "refresh");
235
+ }
236
+ });
237
+ const loadMore = useCallback(() => {
238
+ if (!nextStartDateRef.current || isLoading) return;
239
+ const start = nextStartDateRef.current;
240
+ const end = formatLocalScheduleDate(
241
+ addLocalScheduleDays(
242
+ parseBookingLocalDate(start),
243
+ BOOKING_FETCH_CHUNK_DAYS
244
+ )
245
+ );
246
+ fetchDates(start, end, true);
247
+ }, [fetchDates, isLoading]);
248
+ return {
249
+ availableDates,
250
+ isLoading,
251
+ isRefreshing,
252
+ error,
253
+ hasMore,
254
+ loadMore,
255
+ loadedRange
256
+ };
257
+ }
258
+ var AVAILABILITY_REFRESH_INTERVAL_MS2 = 3e4;
259
+ var AVAILABILITY_REFRESH_JITTER_MS2 = 5e3;
260
+ function useAvailableSlots({
261
+ siteId: _siteId,
262
+ // siteId now comes from BlockApiProvider context
263
+ serviceId,
264
+ resourceId,
265
+ selectedDate,
266
+ timezone
267
+ }) {
268
+ const api = useBlockApi();
269
+ const [slots, setSlots] = useState([]);
270
+ const [isLoading, setIsLoading] = useState(false);
271
+ const [isRefreshing, setIsRefreshing] = useState(false);
272
+ const [hasLoaded, setHasLoaded] = useState(false);
273
+ const [error, setError] = useState(null);
274
+ const requestIdRef = useRef(0);
275
+ const fetchSlots = useCallback(
276
+ async (mode) => {
277
+ const requestId = ++requestIdRef.current;
278
+ if (!selectedDate || !serviceId) {
279
+ setSlots([]);
280
+ setIsLoading(false);
281
+ setIsRefreshing(false);
282
+ setHasLoaded(false);
283
+ setError(null);
284
+ return;
285
+ }
286
+ if (mode === "refresh") {
287
+ setIsRefreshing(true);
288
+ } else {
289
+ setIsLoading(true);
290
+ setIsRefreshing(false);
291
+ setSlots([]);
292
+ }
293
+ setError(null);
294
+ try {
295
+ const result = await api.getAvailableSlots({
296
+ serviceId,
297
+ startDate: selectedDate,
298
+ endDate: selectedDate,
299
+ timezone,
300
+ resourceId
301
+ });
302
+ if (requestId !== requestIdRef.current) return;
303
+ setSlots(
304
+ result.slots.map((slot) => ({
305
+ ...slot,
306
+ resourceId: asAppointmentResourceId(slot.resourceId)
307
+ }))
308
+ );
309
+ setHasLoaded(true);
310
+ } catch (err2) {
311
+ if (requestId !== requestIdRef.current) return;
312
+ setError(
313
+ err2 instanceof Error ? err2.message : "Failed to load available times"
314
+ );
315
+ } finally {
316
+ if (mode === "refresh" && requestId === requestIdRef.current) {
317
+ setIsRefreshing(false);
318
+ } else if (requestId === requestIdRef.current) {
319
+ setIsLoading(false);
320
+ }
321
+ }
322
+ },
323
+ [selectedDate, serviceId, resourceId, timezone, api]
324
+ );
325
+ useEffect(() => {
326
+ void fetchSlots("load");
327
+ return () => {
328
+ requestIdRef.current += 1;
329
+ };
330
+ }, [fetchSlots]);
331
+ useVisibleTabPolling({
332
+ enabled: Boolean(selectedDate && serviceId),
333
+ intervalMs: AVAILABILITY_REFRESH_INTERVAL_MS2,
334
+ jitterMs: AVAILABILITY_REFRESH_JITTER_MS2,
335
+ onPoll: () => fetchSlots("refresh")
336
+ });
337
+ return { slots, isLoading, isRefreshing, hasLoaded, error };
338
+ }
339
+
340
+ // ../blocks/src/system/runtime/hooks/useBookingSubmission.state.ts
341
+ var initialBookingSubmissionState = {
342
+ status: { tag: "idle" },
343
+ feedback: { tag: "idle" }
344
+ };
345
+ function bookingSubmissionReducer(state, action) {
346
+ switch (action.type) {
347
+ case "submission-started":
348
+ return {
349
+ ...state,
350
+ status: { tag: "submitting" },
351
+ feedback: { tag: "idle" }
352
+ };
353
+ case "submission-succeeded":
354
+ return {
355
+ status: { tag: "idle" },
356
+ feedback: { tag: "success", message: action.message }
357
+ };
358
+ case "submission-failed":
359
+ case "issue-reported":
360
+ return {
361
+ status: { tag: "idle" },
362
+ feedback: { tag: "error", message: action.message }
363
+ };
364
+ case "feedback-cleared":
365
+ return {
366
+ ...state,
367
+ feedback: { tag: "idle" }
368
+ };
369
+ default:
370
+ return assertNever(action);
371
+ }
372
+ }
373
+ function selectIsSubmitting(state) {
374
+ return state.status.tag === "submitting";
375
+ }
376
+
377
+ // ../blocks/src/system/runtime/hooks/useBookingSubmission.ts
378
+ function useBookingSubmission(_siteId) {
379
+ const api = useBlockApi();
380
+ const [state, dispatch] = useReducer(
381
+ bookingSubmissionReducer,
382
+ initialBookingSubmissionState
383
+ );
384
+ const submit = async (data) => {
385
+ dispatch({ type: "submission-started" });
386
+ try {
387
+ const response = data.kind === "appointment_package" ? await api.submitBookingWithPackage({
388
+ formId: data.formId,
389
+ serviceId: data.serviceId,
390
+ resourceId: data.resourceId || null,
391
+ startAt: data.startAt,
392
+ endAt: data.endAt,
393
+ customFields: data.customFields,
394
+ timezone: data.timezone,
395
+ customerPassId: data.customerPassId
396
+ }) : await api.submitBooking({
397
+ formId: data.formId,
398
+ serviceId: data.serviceId,
399
+ resourceId: data.resourceId || null,
400
+ startAt: data.startAt,
401
+ endAt: data.endAt,
402
+ customFields: data.customFields,
403
+ timezone: data.timezone,
404
+ discountCode: data.discountCode,
405
+ selectedCollectionOptionKind: data.selectedCollectionOptionKind,
406
+ altchaSolution: data.altchaSolution ?? null,
407
+ successUrl: data.successUrl,
408
+ cancelUrl: data.cancelUrl
409
+ });
410
+ dispatch({
411
+ type: "submission-succeeded",
412
+ message: response.type === "checkout_redirect" ? "Redirecting to secure checkout..." : "Booking submitted successfully."
413
+ });
414
+ return okWith(response);
415
+ } catch (error) {
416
+ console.error("Booking submission error:", error);
417
+ const message = error instanceof Error ? error.message : "Failed to book appointment";
418
+ dispatch({
419
+ type: "submission-failed",
420
+ message
421
+ });
422
+ return err(message);
423
+ }
424
+ };
425
+ return {
426
+ submit,
427
+ reportIssue: (message) => dispatch({ type: "issue-reported", message }),
428
+ clearFeedback: () => dispatch({ type: "feedback-cleared" }),
429
+ feedback: state.feedback,
430
+ isSubmitting: selectIsSubmitting(state)
431
+ };
432
+ }
433
+
434
+ // ../blocks/src/system/runtime/shared/bookingPaymentOptionCopy.ts
435
+ function buildUpfrontBookingPaymentOptionCopy(formattedTotal) {
436
+ return {
437
+ title: "Pay in full",
438
+ description: "Pay everything today and your booking is confirmed.",
439
+ ctaLabel: `Pay ${formattedTotal} now`
440
+ };
441
+ }
442
+ function buildDeferredManualBookingPaymentOptionCopy() {
443
+ return {
444
+ title: "Pay offline",
445
+ description: "Book now and pay the business later, outside the platform.",
446
+ helperText: "Nothing is due today.",
447
+ ctaLabel: "Book now"
448
+ };
449
+ }
450
+ function buildInstalmentBookingPaymentOptionCopy(input) {
451
+ return {
452
+ title: "Payment plan",
453
+ description: describeInstalmentPlanCopy(input),
454
+ helperText: "You can pay the rest early any time in your customer portal.",
455
+ ctaLabel: `Pay ${input.formattedDueToday} now`
456
+ };
457
+ }
458
+ function describeInstalmentPlanCopy(input) {
459
+ if (input.depositRefundability) {
460
+ return `Pay ${input.formattedDueToday} today, then continue with scheduled payments. Your deposit is ${formatDepositRefundabilityLabel(input.depositRefundability)}.`;
461
+ }
462
+ if (input.futurePaymentCount === 0) {
463
+ return `Pay ${input.formattedDueToday} today.`;
464
+ }
465
+ if (input.futurePaymentCount === 1) {
466
+ return `Pay ${input.formattedDueToday} today, then one more payment later.`;
467
+ }
468
+ const formattedFirstFuture = input.formattedFirstFuture ?? input.formattedDueToday;
469
+ return `Pay ${input.formattedDueToday} today, then ${input.futurePaymentCount} more ${input.cadenceLabel} payments from ${formattedFirstFuture}.`;
470
+ }
471
+ function buildFlexibleBalanceBookingPaymentOptionCopy(input) {
472
+ return {
473
+ title: "Deposit now, pay the rest later",
474
+ description: `Pay ${input.formattedDueToday} today, then pay the remaining balance any time before ${input.dueDateLabel}. Your deposit is ${formatDepositRefundabilityLabel(input.depositRefundability)}.`,
475
+ helperText: "You can return in your customer portal to pay the rest before the due date.",
476
+ ctaLabel: `Pay ${input.formattedDueToday} now`
477
+ };
478
+ }
479
+ function formatDepositRefundabilityLabel(depositRefundability) {
480
+ return depositRefundability === "non_refundable" ? "non-refundable" : "refundable";
481
+ }
482
+
483
+ // ../blocks/src/system/runtime/shared/bookingPaymentOptions.ts
484
+ function buildBookingPaymentSelectionViewModel(input) {
485
+ const currency = input.priceCurrency ?? "USD";
486
+ const totalCents = Math.max(0, input.priceAmount ?? 0);
487
+ const allowedOptions = input.paymentTerms?.allowedOptions ?? [{ kind: "upfront" }];
488
+ const mode = allowedOptions.length > 1 ? "choice_required" : "single_option";
489
+ return {
490
+ mode,
491
+ sectionTitle: mode === "choice_required" ? "Choose how you'd like to pay" : "Payment",
492
+ serviceTitle: input.serviceTitle,
493
+ options: allowedOptions.map((option) => buildBookingPaymentOptionViewModel({
494
+ option,
495
+ totalCents,
496
+ currency,
497
+ bookingStartAt: input.bookingStartAt,
498
+ locale: input.locale
499
+ }))
500
+ };
501
+ }
502
+ function buildBookingPaymentOptionViewModel(input) {
503
+ switch (input.option.kind) {
504
+ case "upfront": {
505
+ const formattedTotal = formatPrice(input.totalCents, input.currency, input.locale);
506
+ const copy = buildUpfrontBookingPaymentOptionCopy(formattedTotal);
507
+ return {
508
+ kind: "upfront",
509
+ title: copy.title,
510
+ description: copy.description,
511
+ summaryRows: [
512
+ { label: "Total", value: formattedTotal },
513
+ { label: "Due today", value: formattedTotal }
514
+ ],
515
+ ctaLabel: copy.ctaLabel
516
+ };
517
+ }
518
+ case "deferred_manual": {
519
+ const formattedTotal = formatPrice(input.totalCents, input.currency, input.locale);
520
+ const copy = buildDeferredManualBookingPaymentOptionCopy();
521
+ return {
522
+ kind: "deferred_manual",
523
+ title: copy.title,
524
+ description: copy.description,
525
+ summaryRows: [
526
+ { label: "Total", value: formattedTotal },
527
+ { label: "Due today", value: formatPrice(0, input.currency, input.locale) }
528
+ ],
529
+ helperText: copy.helperText,
530
+ ctaLabel: copy.ctaLabel
531
+ };
532
+ }
533
+ case "instalment_plan": {
534
+ const schedulePreview = buildInstalmentSchedulePreview({
535
+ totalCents: input.totalCents,
536
+ option: input.option,
537
+ bookingStartAt: input.bookingStartAt
538
+ });
539
+ const dueToday = schedulePreview[0]?.amountCents ?? input.totalCents;
540
+ const futureRows = schedulePreview.slice(1);
541
+ const formattedTotal = formatPrice(input.totalCents, input.currency, input.locale);
542
+ const formattedDueToday = formatPrice(dueToday, input.currency, input.locale);
543
+ const depositRefundability = getBookingInstalmentDepositRefundability(input.option.plan);
544
+ const copy = buildInstalmentBookingPaymentOptionCopy({
545
+ formattedDueToday,
546
+ futurePaymentCount: futureRows.length,
547
+ formattedFirstFuture: futureRows[0] ? formatPrice(futureRows[0].amountCents, input.currency, input.locale) : null,
548
+ cadenceLabel: getPaymentCadenceLabel(input.option.plan),
549
+ depositRefundability
550
+ });
551
+ return {
552
+ kind: "instalment_plan",
553
+ title: copy.title,
554
+ description: copy.description,
555
+ summaryRows: [
556
+ { label: "Total", value: formattedTotal },
557
+ { label: "Due today", value: formattedDueToday },
558
+ ...depositRefundability ? [{
559
+ label: "Deposit policy",
560
+ value: depositRefundability === "non_refundable" ? "Non-refundable" : "Refundable"
561
+ }] : [],
562
+ ...futureRows.map((row) => ({
563
+ label: formatScheduleDate(row.dueAt, input.locale),
564
+ value: formatPrice(row.amountCents, input.currency, input.locale)
565
+ }))
566
+ ],
567
+ helperText: copy.helperText,
568
+ ctaLabel: copy.ctaLabel
569
+ };
570
+ }
571
+ case "flexible_balance": {
572
+ const dueToday = resolveFlexibleBalanceInitialPaymentCents({
573
+ totalAmountCents: input.totalCents,
574
+ config: input.option.config
575
+ });
576
+ const formattedTotal = formatPrice(input.totalCents, input.currency, input.locale);
577
+ const formattedDueToday = formatPrice(dueToday, input.currency, input.locale);
578
+ const dueAt = resolveFlexibleBalanceDueAt({
579
+ bookingStartAt: input.bookingStartAt ?? (/* @__PURE__ */ new Date()).toISOString(),
580
+ dueDateRule: input.option.config.dueDateRule
581
+ });
582
+ const copy = buildFlexibleBalanceBookingPaymentOptionCopy({
583
+ formattedDueToday,
584
+ dueDateLabel: formatScheduleDate(dueAt, input.locale),
585
+ depositRefundability: input.option.config.depositRefundability
586
+ });
587
+ return {
588
+ kind: "flexible_balance",
589
+ title: copy.title,
590
+ description: copy.description,
591
+ summaryRows: [
592
+ { label: "Total", value: formattedTotal },
593
+ { label: "Due today", value: formattedDueToday },
594
+ { label: "Balance due by", value: formatScheduleDate(dueAt, input.locale) },
595
+ {
596
+ label: "Deposit policy",
597
+ value: input.option.config.depositRefundability === "non_refundable" ? "Non-refundable" : "Refundable"
598
+ }
599
+ ],
600
+ helperText: copy.helperText,
601
+ ctaLabel: copy.ctaLabel
602
+ };
603
+ }
604
+ default:
605
+ return assertNever(input.option);
606
+ }
607
+ }
608
+ function buildInstalmentSchedulePreview(input) {
609
+ const startAt = input.bookingStartAt ?? (/* @__PURE__ */ new Date()).toISOString();
610
+ switch (input.option.plan.kind) {
611
+ case "deposit_plus_one_remainder": {
612
+ const deposit = clampConfiguredAmount(input.option.plan.depositAmountCents, input.totalCents);
613
+ const remainder = Math.max(0, input.totalCents - deposit);
614
+ return compactSchedulePreview([
615
+ { dueAt: startAt, amountCents: deposit },
616
+ {
617
+ dueAt: addIntervalPreservingMonthAnchor(
618
+ startAt,
619
+ input.option.plan.remainderDueCount,
620
+ input.option.plan.intervalUnit
621
+ ),
622
+ amountCents: remainder
623
+ }
624
+ ]);
625
+ }
626
+ case "deposit_plus_scheduled_instalments": {
627
+ const plan = input.option.plan;
628
+ const deposit = clampConfiguredAmount(plan.depositAmountCents, input.totalCents);
629
+ const remaining = Math.max(0, input.totalCents - deposit);
630
+ const instalments = splitAmountWithRemainderInFirst(
631
+ remaining,
632
+ plan.instalmentCount
633
+ );
634
+ return compactSchedulePreview([
635
+ { dueAt: startAt, amountCents: deposit },
636
+ ...instalments.map((amountCents, index) => ({
637
+ dueAt: addIntervalPreservingMonthAnchor(
638
+ startAt,
639
+ plan.intervalCount * (index + 1),
640
+ plan.intervalUnit
641
+ ),
642
+ amountCents
643
+ }))
644
+ ]);
645
+ }
646
+ case "equal_scheduled_instalments": {
647
+ const plan = input.option.plan;
648
+ const instalments = splitAmountWithRemainderInFirst(
649
+ input.totalCents,
650
+ plan.instalmentCount
651
+ );
652
+ return compactSchedulePreview(instalments.map((amountCents, index) => ({
653
+ dueAt: index === 0 ? startAt : addIntervalPreservingMonthAnchor(
654
+ startAt,
655
+ plan.intervalCount * index,
656
+ plan.intervalUnit
657
+ ),
658
+ amountCents
659
+ })));
660
+ }
661
+ default:
662
+ return assertNever(input.option.plan);
663
+ }
664
+ }
665
+ function getPaymentCadenceLabel(plan) {
666
+ const intervalCount = getBookingInstalmentPlanIntervalCount(plan);
667
+ const unitLabel = intervalCount === 1 ? plan.intervalUnit : `${plan.intervalUnit}s`;
668
+ return intervalCount === 1 ? unitLabel : `every ${intervalCount} ${unitLabel}`;
669
+ }
670
+ function compactSchedulePreview(schedule) {
671
+ return omitZeroAmountFutureRows(schedule);
672
+ }
673
+ function formatScheduleDate(isoString, locale) {
674
+ const date = new Date(isoString);
675
+ if (!Number.isFinite(date.getTime())) {
676
+ return isoString;
677
+ }
678
+ return new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date);
679
+ }
680
+
681
+ // ../blocks/src/system/runtime/nodes/booking-form.headless.state.ts
682
+ var asBookingFieldId = (id) => id;
683
+ function createEmptyBookingValidationErrors() {
684
+ return { formError: null, fieldErrors: {} };
685
+ }
686
+ function createFormBookingValidationError(formError) {
687
+ return { formError, fieldErrors: {} };
688
+ }
689
+ function createFieldBookingValidationErrors(fieldErrors) {
690
+ return { formError: null, fieldErrors };
691
+ }
692
+ function hasBookingValidationErrors(errors) {
693
+ return Boolean(errors.formError) || Object.keys(errors.fieldErrors).length > 0;
694
+ }
695
+ function clearBookingFormError(errors) {
696
+ return errors.formError ? { ...errors, formError: null } : errors;
697
+ }
698
+ function clearBookingFieldError(errors, fieldId) {
699
+ if (!(fieldId in errors.fieldErrors)) return errors;
700
+ return {
701
+ ...errors,
702
+ fieldErrors: omitRecordKey(errors.fieldErrors, fieldId)
703
+ };
704
+ }
705
+ function createInitialBookingFlowReducerState(input) {
706
+ const initialData = input.initialData ?? {};
707
+ return {
708
+ phase: "editing",
709
+ currentStepId: "service-selection",
710
+ selectedServiceId: toOptionalAppointmentServiceId(
711
+ toOptionalString(initialData.serviceId) ?? input.defaultServiceId
712
+ ),
713
+ selectedResourceId: toOptionalAppointmentResourceId(
714
+ toOptionalString(initialData.resourceId) ?? input.defaultResourceId
715
+ ),
716
+ selectedDate: toOptionalString(initialData.selectedDate),
717
+ selectedSlot: null,
718
+ fieldValues: collectInitialFieldValues(initialData),
719
+ touchedFields: {},
720
+ discountCode: toOptionalString(initialData.discountCode) ?? "",
721
+ selectedCollectionOptionKind: toSelectedCollectionOptionKind(
722
+ initialData.selectedCollectionOptionKind
723
+ ),
724
+ selectedAppointmentPackageId: toOptionalCustomerPassId(
725
+ toOptionalString(initialData.selectedAppointmentPackageId)
726
+ ),
727
+ altchaSolution: null,
728
+ validationErrors: createEmptyBookingValidationErrors(),
729
+ feedback: { kind: "none" }
730
+ };
731
+ }
732
+ function collectInitialFieldValues(initialData) {
733
+ const values = {};
734
+ for (const [key, value] of Object.entries(initialData)) {
735
+ if (BOOKING_FORM_DATA_RESERVED_KEYS.has(key) || !isBookingFieldValue(value)) {
736
+ continue;
737
+ }
738
+ values[key] = value;
739
+ }
740
+ return values;
741
+ }
742
+ function toOptionalString(value) {
743
+ if (typeof value !== "string") return void 0;
744
+ const trimmed = value.trim();
745
+ return trimmed.length > 0 ? trimmed : void 0;
746
+ }
747
+ function toOptionalBrandedId(value, brand) {
748
+ const trimmed = toOptionalString(value);
749
+ return trimmed ? brand(trimmed) : void 0;
750
+ }
751
+ function toOptionalAppointmentServiceId(value) {
752
+ return toOptionalBrandedId(value, asAppointmentServiceId);
753
+ }
754
+ function toOptionalAppointmentResourceId(value) {
755
+ return toOptionalBrandedId(value, asAppointmentResourceId);
756
+ }
757
+ function toOptionalCustomerPassId(value) {
758
+ return toOptionalBrandedId(value, asCustomerPassId);
759
+ }
760
+ function isBookingFieldValue(value) {
761
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value) && value.every((item) => typeof item === "string");
762
+ }
763
+ function bookingFlowReducer(state, event) {
764
+ switch (event.type) {
765
+ case "service-selected":
766
+ return {
767
+ ...state,
768
+ selectedServiceId: event.serviceId,
769
+ selectedResourceId: void 0,
770
+ selectedDate: void 0,
771
+ selectedSlot: null,
772
+ selectedAppointmentPackageId: void 0,
773
+ selectedCollectionOptionKind: void 0,
774
+ validationErrors: clearBookingFormError(state.validationErrors)
775
+ };
776
+ case "resource-selected":
777
+ return {
778
+ ...state,
779
+ selectedResourceId: event.resourceId ?? void 0,
780
+ selectedDate: void 0,
781
+ selectedSlot: null,
782
+ validationErrors: clearBookingFormError(state.validationErrors)
783
+ };
784
+ case "date-selected":
785
+ return {
786
+ ...state,
787
+ selectedDate: event.date ?? void 0,
788
+ selectedSlot: null,
789
+ feedback: clearAvailabilityFeedback(state.feedback),
790
+ validationErrors: clearBookingFormError(state.validationErrors)
791
+ };
792
+ case "slot-selected":
793
+ return {
794
+ ...state,
795
+ selectedSlot: event.slot,
796
+ feedback: clearAvailabilityFeedback(state.feedback),
797
+ validationErrors: clearBookingFormError(state.validationErrors)
798
+ };
799
+ case "field-updated":
800
+ return {
801
+ ...state,
802
+ fieldValues: {
803
+ ...state.fieldValues,
804
+ [event.fieldId]: event.value
805
+ },
806
+ validationErrors: clearBookingFieldError(
807
+ clearBookingFormError(state.validationErrors),
808
+ event.fieldId
809
+ )
810
+ };
811
+ case "field-touched":
812
+ return {
813
+ ...state,
814
+ touchedFields: {
815
+ ...state.touchedFields,
816
+ [event.fieldId]: true
817
+ }
818
+ };
819
+ case "payment-option-selected":
820
+ return {
821
+ ...state,
822
+ selectedCollectionOptionKind: event.kind ?? void 0,
823
+ selectedAppointmentPackageId: void 0,
824
+ validationErrors: clearBookingFormError(state.validationErrors)
825
+ };
826
+ case "appointment-package-selected":
827
+ return {
828
+ ...state,
829
+ selectedAppointmentPackageId: event.packageId ?? void 0,
830
+ selectedCollectionOptionKind: void 0,
831
+ validationErrors: clearBookingFormError(state.validationErrors)
832
+ };
833
+ case "discount-code-changed":
834
+ return {
835
+ ...state,
836
+ discountCode: event.code,
837
+ validationErrors: clearBookingFormError(state.validationErrors)
838
+ };
839
+ case "altcha-solution-set":
840
+ return {
841
+ ...state,
842
+ altchaSolution: event.solution,
843
+ validationErrors: clearBookingFormError(state.validationErrors)
844
+ };
845
+ case "altcha-solution-invalidated":
846
+ return { ...state, altchaSolution: null };
847
+ case "step-changed":
848
+ return {
849
+ ...state,
850
+ currentStepId: event.stepId,
851
+ validationErrors: clearBookingFormError(state.validationErrors)
852
+ };
853
+ case "validation-errors-set":
854
+ return { ...state, validationErrors: event.errors };
855
+ case "feedback-set":
856
+ return {
857
+ ...state,
858
+ phase: event.feedback.kind === "success" ? "completed" : "editing",
859
+ feedback: event.feedback
860
+ };
861
+ case "availability-selection-invalidated":
862
+ if (state.phase !== "editing") {
863
+ return state;
864
+ }
865
+ return {
866
+ ...state,
867
+ currentStepId: "datetime-selection",
868
+ selectedDate: event.invalidation.kind === "date_unavailable" ? void 0 : state.selectedDate,
869
+ selectedSlot: null,
870
+ feedback: {
871
+ kind: "error",
872
+ scope: "availability",
873
+ message: event.invalidation.message
874
+ },
875
+ validationErrors: clearBookingFormError(state.validationErrors)
876
+ };
877
+ case "feedback-cleared":
878
+ return { ...state, phase: "editing", feedback: { kind: "none" } };
879
+ case "submission-started":
880
+ return { ...state, phase: "submitting" };
881
+ case "submission-finished":
882
+ return state.phase === "submitting" ? { ...state, phase: "editing" } : state;
883
+ case "reset":
884
+ return event.initial;
885
+ default:
886
+ return assertNever(event);
887
+ }
888
+ }
889
+ function clearAvailabilityFeedback(feedback) {
890
+ return feedback.kind === "error" && feedback.scope === "availability" ? { kind: "none" } : feedback;
891
+ }
892
+ function resolveAvailabilitySelectionInvalidation(input) {
893
+ if (input.selectedDate && !input.availableDates.includes(input.selectedDate)) {
894
+ return {
895
+ kind: "date_unavailable",
896
+ message: "That date is no longer available. Please choose another date."
897
+ };
898
+ }
899
+ if (input.selectedSlot && !input.slots.some((slot) => slot.key === input.selectedSlot?.key)) {
900
+ return {
901
+ kind: "slot_unavailable",
902
+ message: "That time is no longer available. Please choose another time."
903
+ };
904
+ }
905
+ return { kind: "preserve" };
906
+ }
907
+ function toBookingSlotView(slot) {
908
+ return {
909
+ key: `${slot.startAt}|${slot.endAt}|${slot.resourceId}`,
910
+ startAt: slot.startAt,
911
+ endAt: slot.endAt,
912
+ resourceId: asAppointmentResourceId(slot.resourceId),
913
+ ...slot.resourceName ? { resourceName: slot.resourceName } : {},
914
+ ...slot.label ? { label: slot.label } : {}
915
+ };
916
+ }
917
+ function toBookingSlotViews(slots) {
918
+ return slots.map(toBookingSlotView);
919
+ }
920
+ function toBookingFormData(state) {
921
+ return {
922
+ ...state.fieldValues,
923
+ serviceId: state.selectedServiceId,
924
+ resourceId: state.selectedResourceId ?? state.selectedSlot?.resourceId,
925
+ selectedDate: state.selectedDate,
926
+ selectedSlot: state.selectedSlot?.startAt,
927
+ discountCode: state.discountCode || void 0,
928
+ selectedCollectionOptionKind: state.selectedCollectionOptionKind,
929
+ selectedAppointmentPackageId: state.selectedAppointmentPackageId
930
+ };
931
+ }
932
+ function buildBookingStepResolution(input) {
933
+ const defaultServiceId = input.defaultServiceId ?? resolveDefaultBookingServiceId(input.form, input.services);
934
+ const paymentResolution = resolveBookingPaymentStep({
935
+ data: input.data,
936
+ services: input.services,
937
+ defaultServiceId
938
+ });
939
+ if (!input.form) return { steps: [], paymentResolution };
940
+ const hasValidConfiguredService = Boolean(defaultServiceId) && input.services.some((service) => service.id === defaultServiceId);
941
+ const needsServiceSelection = Boolean(input.requiresServiceSelection) || !hasValidConfiguredService && input.services.length > 1 || Boolean(input.requiresResourceSelection) || Boolean(
942
+ input.form.settings?.serviceIds && input.form.settings.serviceIds.length > 1
943
+ );
944
+ const selectedService = paymentResolution.service;
945
+ const needsPayment = paymentResolution.mode !== "hidden" || Boolean(
946
+ input.enableAppointmentPackages && selectedService && (selectedService.priceAmount ?? 0) > 0
947
+ );
948
+ const hasFields = Boolean(input.form.schema?.fields.length);
949
+ return {
950
+ steps: [
951
+ {
952
+ id: "service-selection",
953
+ label: "Select Service",
954
+ visible: needsServiceSelection
955
+ },
956
+ { id: "datetime-selection", label: "Select Date & Time", visible: true },
957
+ { id: "payment-selection", label: "Payment", visible: needsPayment },
958
+ { id: "custom-fields", label: "Your Information", visible: hasFields }
959
+ ],
960
+ paymentResolution
961
+ };
962
+ }
963
+ function validateBookingStep(input) {
964
+ switch (input.stepId) {
965
+ case "service-selection": {
966
+ let formError = null;
967
+ if (!input.data.serviceId) {
968
+ formError = "Please select a service";
969
+ }
970
+ if (input.requiresResourceSelection && !(input.data.resourceId ?? input.defaultResourceId)) {
971
+ formError = "Please select a practitioner";
972
+ }
973
+ return formError ? { ok: false, errors: createFormBookingValidationError(formError) } : { ok: true };
974
+ }
975
+ case "datetime-selection":
976
+ return input.data.selectedSlot ? { ok: true } : {
977
+ ok: false,
978
+ errors: createFormBookingValidationError(
979
+ "Please select a date and time"
980
+ )
981
+ };
982
+ case "payment-selection": {
983
+ if (input.data.selectedAppointmentPackageId) return { ok: true };
984
+ const resolution = input.paymentResolution ?? resolveBookingPaymentStep({
985
+ data: input.data,
986
+ services: input.services,
987
+ defaultServiceId: input.defaultServiceId
988
+ });
989
+ if (resolution.mode === "hidden" || resolution.mode === "disclosure_only") {
990
+ return { ok: true };
991
+ }
992
+ if (resolution.mode === "unsupported") {
993
+ return {
994
+ ok: false,
995
+ errors: createFormBookingValidationError(
996
+ resolution.errorMessage ?? "This service cannot be booked right now."
997
+ )
998
+ };
999
+ }
1000
+ return input.data.selectedCollectionOptionKind ? { ok: true } : {
1001
+ ok: false,
1002
+ errors: createFormBookingValidationError(
1003
+ "Please choose a payment option"
1004
+ )
1005
+ };
1006
+ }
1007
+ case "custom-fields": {
1008
+ const errors = validateBookingFields({
1009
+ fields: input.form?.schema?.fields ?? [],
1010
+ values: input.data
1011
+ });
1012
+ return Object.keys(errors).length > 0 ? { ok: false, errors: createFieldBookingValidationErrors(errors) } : { ok: true };
1013
+ }
1014
+ default:
1015
+ return assertNever(input.stepId);
1016
+ }
1017
+ }
1018
+ function validateBookingFields(input) {
1019
+ const errors = {};
1020
+ for (const field of input.fields) {
1021
+ const error = validateBookingField(field, input.values[field.id]);
1022
+ if (error) {
1023
+ errors[field.id] = error;
1024
+ }
1025
+ }
1026
+ return errors;
1027
+ }
1028
+ function validateBookingField(field, value) {
1029
+ if (field.required && isBlankFieldValue(value)) {
1030
+ return `${field.label} is required`;
1031
+ }
1032
+ if (isBlankFieldValue(value)) return null;
1033
+ const strValue = typeof value === "string" ? value : String(value);
1034
+ switch (field.type) {
1035
+ case "email":
1036
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(strValue) ? null : "Please enter a valid email address";
1037
+ case "url":
1038
+ try {
1039
+ new URL(strValue);
1040
+ return null;
1041
+ } catch {
1042
+ return "Please enter a valid URL";
1043
+ }
1044
+ case "tel":
1045
+ return /^[+\d\s\-()]+$/.test(strValue) ? null : "Please enter a valid phone number";
1046
+ case "number": {
1047
+ const num = Number(value);
1048
+ if (Number.isNaN(num)) return "Please enter a valid number";
1049
+ if (field.min !== void 0 && num < field.min) {
1050
+ return `Value must be at least ${field.min}`;
1051
+ }
1052
+ if (field.max !== void 0 && num > field.max) {
1053
+ return `Value must be at most ${field.max}`;
1054
+ }
1055
+ return null;
1056
+ }
1057
+ }
1058
+ if (typeof value === "string") {
1059
+ if (field.minLength && value.length < field.minLength) {
1060
+ return `Must be at least ${field.minLength} characters`;
1061
+ }
1062
+ if (field.maxLength && value.length > field.maxLength) {
1063
+ return `Must be at most ${field.maxLength} characters`;
1064
+ }
1065
+ if (field.pattern) {
1066
+ try {
1067
+ const regex = new RegExp(field.pattern);
1068
+ if (!regex.test(value)) return "Please match the required format";
1069
+ } catch {
1070
+ return "Please match the required format";
1071
+ }
1072
+ }
1073
+ }
1074
+ return null;
1075
+ }
1076
+ function deriveBookingFlowSteps(input) {
1077
+ const stepResolution = buildBookingStepResolution({
1078
+ form: input.form,
1079
+ services: input.services,
1080
+ data: input.data,
1081
+ defaultServiceId: input.defaultServiceId,
1082
+ requiresServiceSelection: input.requiresServiceSelection,
1083
+ requiresResourceSelection: input.requiresResourceSelection,
1084
+ enableAppointmentPackages: input.enableAppointmentPackages
1085
+ });
1086
+ const stepPlan = stepResolution.steps.filter((step) => step.visible);
1087
+ const currentStepIndex = Math.max(
1088
+ 0,
1089
+ stepPlan.findIndex((step) => step.id === input.currentStepId)
1090
+ );
1091
+ const validations = /* @__PURE__ */ new Map();
1092
+ for (const step of stepPlan) {
1093
+ validations.set(
1094
+ step.id,
1095
+ validateBookingStep({
1096
+ stepId: step.id,
1097
+ form: input.form,
1098
+ services: input.services,
1099
+ data: input.data,
1100
+ defaultServiceId: input.defaultServiceId,
1101
+ defaultResourceId: input.defaultResourceId,
1102
+ requiresResourceSelection: input.requiresResourceSelection,
1103
+ paymentResolution: stepResolution.paymentResolution
1104
+ })
1105
+ );
1106
+ }
1107
+ const steps = stepPlan.map((step, index) => {
1108
+ const validation = validations.get(step.id) ?? { ok: true };
1109
+ const state = index < currentStepIndex ? "complete" : index === currentStepIndex ? "active" : "available";
1110
+ return {
1111
+ id: step.id,
1112
+ label: step.label,
1113
+ state,
1114
+ index,
1115
+ visibleIndex: index,
1116
+ errors: validation.ok ? createEmptyBookingValidationErrors() : validation.errors,
1117
+ canEnter: index <= currentStepIndex,
1118
+ canLeave: validation.ok
1119
+ };
1120
+ });
1121
+ return {
1122
+ steps,
1123
+ currentStepIndex,
1124
+ currentStep: steps[currentStepIndex] ?? null,
1125
+ stepIds: steps.map((step) => step.id),
1126
+ validations
1127
+ };
1128
+ }
1129
+ function buildBookingAvailabilityQuery(input) {
1130
+ if (!input.serviceId) {
1131
+ return {
1132
+ ok: false,
1133
+ reason: "missing-service",
1134
+ key: `missing-service|${input.timezone}`
1135
+ };
1136
+ }
1137
+ if (input.requireDate && !input.selectedDate) {
1138
+ return {
1139
+ ok: false,
1140
+ reason: "missing-date",
1141
+ key: `${input.serviceId}|${input.resourceId ?? "any"}|missing-date|${input.timezone}`
1142
+ };
1143
+ }
1144
+ return {
1145
+ ok: true,
1146
+ key: `${input.serviceId}|${input.resourceId ?? "any"}|${input.selectedDate ?? "all-dates"}|${input.timezone}`,
1147
+ serviceId: input.serviceId,
1148
+ resourceId: input.resourceId,
1149
+ selectedDate: input.selectedDate,
1150
+ timezone: input.timezone
1151
+ };
1152
+ }
1153
+ function deriveBookingFieldViews(input) {
1154
+ return (input.form?.schema?.fields ?? []).map((field) => {
1155
+ const fieldId = asBookingFieldId(field.id);
1156
+ const inputId = `${input.idPrefix}-${fieldId}`;
1157
+ const helpTextId = field.helpText ? `${inputId}-help` : null;
1158
+ const fieldValue = input.values[field.id] ?? null;
1159
+ const localError = input.touched?.[field.id] ? validateBookingField(field, fieldValue) : null;
1160
+ const error = input.errors[field.id] ?? localError;
1161
+ const errorId = error ? `${inputId}-error` : null;
1162
+ const describedBy = [helpTextId, errorId].filter(Boolean).join(" ") || void 0;
1163
+ return {
1164
+ field,
1165
+ id: fieldId,
1166
+ label: field.label,
1167
+ type: field.type,
1168
+ value: fieldValue,
1169
+ required: Boolean(field.required),
1170
+ error,
1171
+ helpText: field.helpText ?? null,
1172
+ inputId,
1173
+ labelId: `${inputId}-label`,
1174
+ helpTextId,
1175
+ errorId,
1176
+ describedBy,
1177
+ ariaInvalid: Boolean(error)
1178
+ };
1179
+ });
1180
+ }
1181
+ function buildBookingSubmitPlan(input) {
1182
+ if (input.requiresResourceSelection && !(input.state.selectedResourceId ?? input.defaultResourceId)) {
1183
+ const message = "Please select a practitioner";
1184
+ return {
1185
+ ok: false,
1186
+ errors: createFormBookingValidationError(message),
1187
+ message
1188
+ };
1189
+ }
1190
+ const fieldErrors = validateBookingFields({
1191
+ fields: input.form?.schema?.fields ?? [],
1192
+ values: input.state.fieldValues
1193
+ });
1194
+ if (Object.keys(fieldErrors).length > 0) {
1195
+ return {
1196
+ ok: false,
1197
+ errors: createFieldBookingValidationErrors(fieldErrors),
1198
+ message: "Please check the highlighted fields."
1199
+ };
1200
+ }
1201
+ const prepared = prepareBookingSubmission({
1202
+ formId: input.formId,
1203
+ data: toBookingFormData({
1204
+ ...input.state,
1205
+ altchaSolution: input.altchaSolution ?? input.state.altchaSolution
1206
+ }),
1207
+ services: input.services,
1208
+ defaultServiceId: input.defaultServiceId,
1209
+ defaultResourceId: input.defaultResourceId,
1210
+ altchaSolution: input.altchaSolution ?? input.state.altchaSolution,
1211
+ viewerTimeZone: input.viewerTimeZone
1212
+ });
1213
+ if (!prepared.ok) {
1214
+ const message = formatBookingSubmissionPreparationError(prepared.error);
1215
+ return {
1216
+ ok: false,
1217
+ errors: createFormBookingValidationError(message),
1218
+ message
1219
+ };
1220
+ }
1221
+ return {
1222
+ ok: true,
1223
+ redirect: input.redirect,
1224
+ value: prepared.value.kind === "appointment_package" ? {
1225
+ ...prepared.value,
1226
+ endAt: input.state.selectedSlot?.endAt ?? prepared.value.endAt,
1227
+ resourceId: prepared.value.resourceId ?? input.state.selectedSlot?.resourceId
1228
+ } : {
1229
+ ...prepared.value,
1230
+ endAt: input.state.selectedSlot?.endAt ?? prepared.value.endAt,
1231
+ resourceId: prepared.value.resourceId ?? input.state.selectedSlot?.resourceId,
1232
+ successUrl: input.successUrl,
1233
+ cancelUrl: input.cancelUrl
1234
+ }
1235
+ };
1236
+ }
1237
+ function resolveAppointmentPackagesModel(input) {
1238
+ if (!input.enabled) {
1239
+ return {
1240
+ kind: "disabled",
1241
+ enabled: false,
1242
+ canLoad: false,
1243
+ unavailableReason: "disabled",
1244
+ state: { tag: "idle" },
1245
+ items: [],
1246
+ selected: null
1247
+ };
1248
+ }
1249
+ if (!input.canLoad) {
1250
+ const unavailableReason = input.unavailableReason === "missing-portal-auth" ? "missing-portal-auth" : "missing-priced-service";
1251
+ return {
1252
+ kind: "unavailable",
1253
+ enabled: true,
1254
+ canLoad: false,
1255
+ unavailableReason,
1256
+ state: { tag: "idle" },
1257
+ items: [],
1258
+ selected: null
1259
+ };
1260
+ }
1261
+ switch (input.state.tag) {
1262
+ case "idle":
1263
+ return {
1264
+ kind: "idle",
1265
+ enabled: true,
1266
+ canLoad: true,
1267
+ unavailableReason: null,
1268
+ state: input.state,
1269
+ items: [],
1270
+ selected: null
1271
+ };
1272
+ case "loading":
1273
+ return {
1274
+ kind: "loading",
1275
+ enabled: true,
1276
+ canLoad: true,
1277
+ unavailableReason: null,
1278
+ state: input.state,
1279
+ items: [],
1280
+ selected: null
1281
+ };
1282
+ case "error":
1283
+ return {
1284
+ kind: "error",
1285
+ enabled: true,
1286
+ canLoad: true,
1287
+ unavailableReason: null,
1288
+ state: input.state,
1289
+ items: [],
1290
+ selected: null
1291
+ };
1292
+ case "loaded": {
1293
+ const items = input.items.map(toHeadlessEligibleAppointmentPackage);
1294
+ const selected = input.selected ? toHeadlessEligibleAppointmentPackage(input.selected) : null;
1295
+ return {
1296
+ kind: "ready",
1297
+ enabled: true,
1298
+ canLoad: true,
1299
+ unavailableReason: null,
1300
+ state: { tag: "loaded", value: items },
1301
+ items,
1302
+ selected
1303
+ };
1304
+ }
1305
+ default:
1306
+ return assertNever(input.state);
1307
+ }
1308
+ }
1309
+ function toHeadlessEligibleAppointmentPackage(appointmentPackage) {
1310
+ return {
1311
+ ...appointmentPackage,
1312
+ customerPassId: asCustomerPassId(appointmentPackage.customerPassId),
1313
+ passId: asPassId(appointmentPackage.passId)
1314
+ };
1315
+ }
1316
+ function resolveHeadlessBookingStatus(input) {
1317
+ if (input.phase === "submitting") return { tag: "submitting" };
1318
+ if (input.phase === "completed" && input.feedback.kind === "success") {
1319
+ return {
1320
+ tag: "success",
1321
+ resourceName: input.feedback.resourceName
1322
+ };
1323
+ }
1324
+ if (input.feedback.kind === "error") {
1325
+ return {
1326
+ tag: "error",
1327
+ message: input.feedback.message
1328
+ };
1329
+ }
1330
+ return { tag: "ready" };
1331
+ }
1332
+ function resolveHeadlessBookingView(input) {
1333
+ if (!input.form) {
1334
+ return {
1335
+ kind: "missing",
1336
+ message: "Booking form not found."
1337
+ };
1338
+ }
1339
+ if (input.feedback.kind === "success") {
1340
+ return {
1341
+ kind: "success",
1342
+ message: input.feedback.message,
1343
+ resourceName: input.feedback.resourceName
1344
+ };
1345
+ }
1346
+ if (input.feedback.kind === "error") {
1347
+ return {
1348
+ kind: "error",
1349
+ message: input.feedback.message
1350
+ };
1351
+ }
1352
+ return { kind: "form" };
1353
+ }
1354
+ function describeUnknownError(error, fallback) {
1355
+ return error instanceof Error ? error.message : fallback;
1356
+ }
1357
+ function isBlankFieldValue(value) {
1358
+ if (value == null) return true;
1359
+ if (typeof value === "string") return value.trim().length === 0;
1360
+ if (value === false) return true;
1361
+ if (Array.isArray(value)) return value.length === 0;
1362
+ return false;
1363
+ }
1364
+ function omitRecordKey(record, key) {
1365
+ if (!(key in record)) return record;
1366
+ const { [key]: _removed, ...rest } = record;
1367
+ return rest;
1368
+ }
1369
+
1370
+ // ../blocks/src/system/runtime/hooks/useHeadlessBookingFlow.ts
1371
+ function useHeadlessBookingFlow(options) {
1372
+ const viewerTimeZone = options.viewerTimeZone ?? getRuntimeTimeZone();
1373
+ const formKey = asFormId(options.formId);
1374
+ const defaultServiceId = toOptionalAppointmentServiceId(
1375
+ options.initialServiceId ?? resolveDefaultBookingServiceId(options.form, options.services) ?? void 0
1376
+ );
1377
+ const defaultResourceId = toOptionalAppointmentResourceId(
1378
+ options.initialResourceId
1379
+ );
1380
+ const initialState = useMemo(
1381
+ () => createInitialBookingFlowReducerState({
1382
+ defaultServiceId,
1383
+ defaultResourceId,
1384
+ initialData: options.initialData
1385
+ }),
1386
+ [defaultResourceId, defaultServiceId, options.initialData]
1387
+ );
1388
+ const [state, dispatch] = useReducer(bookingFlowReducer, initialState);
1389
+ const api = useBlockApi();
1390
+ const [resourcesState, setResourcesState] = useState({ tag: "idle" });
1391
+ const resourcesRequestIdRef = useRef(0);
1392
+ const [appointmentPackages, setAppointmentPackages] = useState({ tag: "idle" });
1393
+ const appointmentPackagesRequestIdRef = useRef(0);
1394
+ const selectedServiceId = state.selectedServiceId ?? defaultServiceId;
1395
+ const selectedResourceId = state.selectedResourceId ?? defaultResourceId;
1396
+ const selectedService = options.services.find((service) => service.id === selectedServiceId) ?? null;
1397
+ const hasPortalBearerAuth = (api.config.authOverrides?.portal ?? api.config.auth).type === "bearer";
1398
+ const canLoadAppointmentPackages = Boolean(
1399
+ options.enableAppointmentPackages && hasPortalBearerAuth && selectedService && (selectedService.priceAmount ?? 0) > 0
1400
+ );
1401
+ const appointmentPackagesUnavailableReason = !options.enableAppointmentPackages ? "disabled" : !selectedService || (selectedService.priceAmount ?? 0) <= 0 ? "missing-priced-service" : !hasPortalBearerAuth ? "missing-portal-auth" : null;
1402
+ const data = useMemo(() => toBookingFormData(state), [state]);
1403
+ const availabilityDatesQuery = useMemo(
1404
+ () => buildBookingAvailabilityQuery({
1405
+ serviceId: selectedServiceId,
1406
+ resourceId: selectedResourceId,
1407
+ timezone: viewerTimeZone
1408
+ }),
1409
+ [selectedResourceId, selectedServiceId, viewerTimeZone]
1410
+ );
1411
+ const availabilitySlotsQuery = useMemo(
1412
+ () => buildBookingAvailabilityQuery({
1413
+ serviceId: selectedServiceId,
1414
+ resourceId: selectedResourceId,
1415
+ selectedDate: state.selectedDate,
1416
+ timezone: viewerTimeZone,
1417
+ requireDate: true
1418
+ }),
1419
+ [selectedResourceId, selectedServiceId, state.selectedDate, viewerTimeZone]
1420
+ );
1421
+ const stepState = useMemo(
1422
+ () => deriveBookingFlowSteps({
1423
+ form: options.form,
1424
+ services: options.services,
1425
+ data,
1426
+ defaultServiceId,
1427
+ defaultResourceId,
1428
+ currentStepId: state.currentStepId,
1429
+ requiresServiceSelection: options.requiresServiceSelection,
1430
+ requiresResourceSelection: options.requiresResourceSelection,
1431
+ enableAppointmentPackages: options.enableAppointmentPackages
1432
+ }),
1433
+ [
1434
+ data,
1435
+ defaultServiceId,
1436
+ defaultResourceId,
1437
+ options.enableAppointmentPackages,
1438
+ options.form,
1439
+ options.requiresResourceSelection,
1440
+ options.requiresServiceSelection,
1441
+ options.services,
1442
+ state.currentStepId
1443
+ ]
1444
+ );
1445
+ const {
1446
+ availableDates,
1447
+ isLoading: isLoadingDates,
1448
+ error: datesError,
1449
+ hasMore,
1450
+ loadMore,
1451
+ loadedRange: availableDatesLoadedRange
1452
+ } = useAvailableDates({
1453
+ siteId: options.siteId,
1454
+ serviceId: availabilityDatesQuery.ok ? availabilityDatesQuery.serviceId : void 0,
1455
+ resourceId: availabilityDatesQuery.ok ? availabilityDatesQuery.resourceId : void 0,
1456
+ timezone: viewerTimeZone,
1457
+ initialDays: 14
1458
+ });
1459
+ const {
1460
+ slots,
1461
+ isLoading: isLoadingSlots,
1462
+ hasLoaded: hasLoadedSlots,
1463
+ error: slotsError
1464
+ } = useAvailableSlots({
1465
+ siteId: options.siteId,
1466
+ serviceId: availabilitySlotsQuery.ok ? availabilitySlotsQuery.serviceId : void 0,
1467
+ resourceId: availabilitySlotsQuery.ok ? availabilitySlotsQuery.resourceId : void 0,
1468
+ selectedDate: availabilitySlotsQuery.ok ? availabilitySlotsQuery.selectedDate ?? "" : "",
1469
+ timezone: viewerTimeZone
1470
+ });
1471
+ const slotViews = useMemo(() => toBookingSlotViews(slots), [slots]);
1472
+ const dateValues = useMemo(
1473
+ () => [...availableDates].sort(),
1474
+ [availableDates]
1475
+ );
1476
+ useEffect(() => {
1477
+ if (state.phase !== "editing") return;
1478
+ const canEvaluateDate = availabilityDatesQuery.ok && Boolean(availableDatesLoadedRange) && !isLoadingDates && !datesError;
1479
+ const canEvaluateSlot = !state.selectedSlot || availabilitySlotsQuery.ok && hasLoadedSlots && !isLoadingSlots && !slotsError;
1480
+ if (!canEvaluateDate || !canEvaluateSlot) return;
1481
+ const invalidation = resolveAvailabilitySelectionInvalidation({
1482
+ selectedDate: state.selectedDate,
1483
+ selectedSlot: state.selectedSlot,
1484
+ availableDates: dateValues,
1485
+ slots: slotViews
1486
+ });
1487
+ if (invalidation.kind === "preserve") return;
1488
+ dispatch({
1489
+ type: "availability-selection-invalidated",
1490
+ invalidation
1491
+ });
1492
+ }, [
1493
+ availabilityDatesQuery.ok,
1494
+ availabilitySlotsQuery.ok,
1495
+ availableDatesLoadedRange,
1496
+ dateValues,
1497
+ datesError,
1498
+ hasLoadedSlots,
1499
+ isLoadingDates,
1500
+ isLoadingSlots,
1501
+ slotViews,
1502
+ slotsError,
1503
+ state.phase,
1504
+ state.selectedDate,
1505
+ state.selectedSlot
1506
+ ]);
1507
+ const fields = useMemo(
1508
+ () => deriveBookingFieldViews({
1509
+ form: options.form,
1510
+ values: state.fieldValues,
1511
+ errors: state.validationErrors.fieldErrors,
1512
+ touched: state.touchedFields,
1513
+ idPrefix: `booking-${options.formId}`
1514
+ }),
1515
+ [
1516
+ options.form,
1517
+ options.formId,
1518
+ state.fieldValues,
1519
+ state.touchedFields,
1520
+ state.validationErrors.fieldErrors
1521
+ ]
1522
+ );
1523
+ useEffect(() => {
1524
+ const requestId = ++resourcesRequestIdRef.current;
1525
+ let isCancelled = false;
1526
+ if (!selectedServiceId) {
1527
+ setResourcesState({ tag: "idle" });
1528
+ return () => {
1529
+ isCancelled = true;
1530
+ resourcesRequestIdRef.current += 1;
1531
+ };
1532
+ }
1533
+ setResourcesState({ tag: "loading" });
1534
+ api.getBookingResources({ serviceId: selectedServiceId }).then((result) => {
1535
+ if (isCancelled || requestId !== resourcesRequestIdRef.current) return;
1536
+ setResourcesState({
1537
+ tag: "loaded",
1538
+ value: (result.options ?? []).map((option) => ({
1539
+ id: asAppointmentResourceId(option.id),
1540
+ label: option.label
1541
+ }))
1542
+ });
1543
+ }).catch((error) => {
1544
+ if (isCancelled || requestId !== resourcesRequestIdRef.current) return;
1545
+ setResourcesState({
1546
+ tag: "error",
1547
+ message: describeUnknownError(
1548
+ error,
1549
+ "Failed to load booking resources."
1550
+ )
1551
+ });
1552
+ });
1553
+ return () => {
1554
+ isCancelled = true;
1555
+ resourcesRequestIdRef.current += 1;
1556
+ };
1557
+ }, [api, selectedServiceId]);
1558
+ useEffect(() => {
1559
+ const requestId = ++appointmentPackagesRequestIdRef.current;
1560
+ let isCancelled = false;
1561
+ if (!canLoadAppointmentPackages || !selectedServiceId) {
1562
+ setAppointmentPackages({ tag: "idle" });
1563
+ return () => {
1564
+ isCancelled = true;
1565
+ appointmentPackagesRequestIdRef.current += 1;
1566
+ };
1567
+ }
1568
+ setAppointmentPackages({ tag: "loading" });
1569
+ api.getEligibleAppointmentPackages({ serviceId: selectedServiceId }).then((result) => {
1570
+ if (isCancelled || requestId !== appointmentPackagesRequestIdRef.current) {
1571
+ return;
1572
+ }
1573
+ setAppointmentPackages({ tag: "loaded", value: result.packages });
1574
+ }).catch((error) => {
1575
+ if (isCancelled || requestId !== appointmentPackagesRequestIdRef.current) {
1576
+ return;
1577
+ }
1578
+ setAppointmentPackages({
1579
+ tag: "error",
1580
+ message: describeUnknownError(
1581
+ error,
1582
+ "Could not check package credits."
1583
+ )
1584
+ });
1585
+ });
1586
+ return () => {
1587
+ isCancelled = true;
1588
+ appointmentPackagesRequestIdRef.current += 1;
1589
+ };
1590
+ }, [api, canLoadAppointmentPackages, selectedServiceId]);
1591
+ useEffect(() => {
1592
+ if (!state.selectedAppointmentPackageId) return;
1593
+ if (appointmentPackages.tag !== "loaded") {
1594
+ if (!canLoadAppointmentPackages) {
1595
+ dispatch({ type: "appointment-package-selected", packageId: null });
1596
+ }
1597
+ return;
1598
+ }
1599
+ const stillEligible = appointmentPackages.value.some(
1600
+ (appointmentPackage) => appointmentPackage.customerPassId === state.selectedAppointmentPackageId
1601
+ );
1602
+ if (!stillEligible) {
1603
+ dispatch({ type: "appointment-package-selected", packageId: null });
1604
+ }
1605
+ }, [
1606
+ appointmentPackages,
1607
+ canLoadAppointmentPackages,
1608
+ state.selectedAppointmentPackageId
1609
+ ]);
1610
+ const resources = useMemo(
1611
+ () => resourcesState.tag === "loaded" ? resourcesState.value : [],
1612
+ [resourcesState]
1613
+ );
1614
+ useEffect(() => {
1615
+ if (options.shouldAutoSelectSingleResource === false) return;
1616
+ if (selectedResourceId) return;
1617
+ if (state.selectedDate || state.selectedSlot) return;
1618
+ if (resources.length !== 1) return;
1619
+ const onlyResource = resources[0];
1620
+ if (onlyResource) {
1621
+ dispatch({ type: "resource-selected", resourceId: onlyResource.id });
1622
+ }
1623
+ }, [
1624
+ options.shouldAutoSelectSingleResource,
1625
+ resources,
1626
+ selectedResourceId,
1627
+ state.selectedDate,
1628
+ state.selectedSlot
1629
+ ]);
1630
+ const appointmentDiscountContext = selectedService && (selectedService.priceAmount ?? 0) > 0 ? { type: "appointment", serviceId: selectedService.id } : null;
1631
+ const discountUi = useDiscountCodeUi({
1632
+ api,
1633
+ discountContext: appointmentDiscountContext,
1634
+ subtotalCents: selectedService?.priceAmount ?? 0
1635
+ });
1636
+ const { submit, clearFeedback } = useBookingSubmission(options.siteId);
1637
+ const submittingRef = useRef(false);
1638
+ const latestRef = useLatestRef({
1639
+ state,
1640
+ data,
1641
+ form: options.form,
1642
+ formId: formKey,
1643
+ services: options.services,
1644
+ defaultServiceId,
1645
+ defaultResourceId,
1646
+ requiresResourceSelection: options.requiresResourceSelection,
1647
+ successMessage: options.successMessage,
1648
+ viewerTimeZone,
1649
+ discountCode: discountUi.discountCode,
1650
+ submit,
1651
+ clearFeedback,
1652
+ setDiscountCode: discountUi.setDiscountCode,
1653
+ navigateToCheckout: options.navigateToCheckout ?? navigateToCheckout,
1654
+ steps: stepState.steps,
1655
+ currentStep: stepState.currentStep,
1656
+ currentStepIndex: stepState.currentStepIndex,
1657
+ stepIds: stepState.stepIds,
1658
+ validations: stepState.validations,
1659
+ initialState
1660
+ });
1661
+ const submitBooking = useCallback(
1662
+ async (submitOptions) => {
1663
+ const latest = latestRef.current;
1664
+ if (submittingRef.current || latest.state.phase === "submitting") {
1665
+ return {
1666
+ ok: false,
1667
+ reason: "blocked",
1668
+ message: "submission-in-flight"
1669
+ };
1670
+ }
1671
+ const plan = buildBookingSubmitPlan({
1672
+ formId: latest.formId,
1673
+ form: latest.form,
1674
+ state: {
1675
+ ...latest.state,
1676
+ discountCode: latest.discountCode
1677
+ },
1678
+ services: latest.services,
1679
+ defaultServiceId: latest.defaultServiceId,
1680
+ defaultResourceId: latest.defaultResourceId,
1681
+ requiresResourceSelection: latest.requiresResourceSelection,
1682
+ altchaSolution: latest.state.altchaSolution,
1683
+ viewerTimeZone: latest.viewerTimeZone,
1684
+ redirect: submitOptions?.redirect ?? "manual",
1685
+ successUrl: submitOptions?.successUrl,
1686
+ cancelUrl: submitOptions?.cancelUrl
1687
+ });
1688
+ if (!plan.ok) {
1689
+ dispatch({ type: "validation-errors-set", errors: plan.errors });
1690
+ return {
1691
+ ok: false,
1692
+ reason: "validation-failed",
1693
+ errors: plan.errors
1694
+ };
1695
+ }
1696
+ submittingRef.current = true;
1697
+ dispatch({ type: "submission-started" });
1698
+ try {
1699
+ const result = await latest.submit(plan.value);
1700
+ if (!result.ok) {
1701
+ dispatch({ type: "altcha-solution-invalidated" });
1702
+ dispatch({
1703
+ type: "feedback-set",
1704
+ feedback: {
1705
+ kind: "error",
1706
+ scope: "submission",
1707
+ message: result.error
1708
+ }
1709
+ });
1710
+ return {
1711
+ ok: false,
1712
+ reason: "submission-failed",
1713
+ message: result.error
1714
+ };
1715
+ }
1716
+ if (result.data.type === "checkout_redirect") {
1717
+ if (plan.redirect === "auto" && typeof window !== "undefined") {
1718
+ latest.navigateToCheckout(result.data.checkoutUrl);
1719
+ }
1720
+ return {
1721
+ ok: true,
1722
+ outcome: {
1723
+ kind: "checkout_redirect",
1724
+ checkoutUrl: result.data.checkoutUrl
1725
+ }
1726
+ };
1727
+ }
1728
+ dispatch({
1729
+ type: "feedback-set",
1730
+ feedback: {
1731
+ kind: "success",
1732
+ message: getBookingSuccessMessage(latest.successMessage),
1733
+ resourceName: result.data.appointment.resourceName
1734
+ }
1735
+ });
1736
+ return {
1737
+ ok: true,
1738
+ outcome: {
1739
+ kind: "booked",
1740
+ appointmentId: result.data.appointment.id,
1741
+ resourceName: result.data.appointment.resourceName
1742
+ }
1743
+ };
1744
+ } finally {
1745
+ submittingRef.current = false;
1746
+ dispatch({ type: "submission-finished" });
1747
+ }
1748
+ },
1749
+ [latestRef]
1750
+ );
1751
+ const actions = useMemo(
1752
+ () => ({
1753
+ selectService: (serviceId) => dispatch({
1754
+ type: "service-selected",
1755
+ serviceId: asAppointmentServiceId(serviceId)
1756
+ }),
1757
+ selectResource: (resourceId) => dispatch({
1758
+ type: "resource-selected",
1759
+ resourceId: resourceId ? asAppointmentResourceId(resourceId) : null
1760
+ }),
1761
+ selectDate: (date) => dispatch({ type: "date-selected", date }),
1762
+ selectSlot: (slot) => dispatch({
1763
+ type: "slot-selected",
1764
+ slot: slot ? toBookingSlotView(slot) : null
1765
+ }),
1766
+ updateField: (fieldId, value) => dispatch({
1767
+ type: "field-updated",
1768
+ fieldId: asBookingFieldId(fieldId),
1769
+ value
1770
+ }),
1771
+ markFieldTouched: (fieldId) => dispatch({ type: "field-touched", fieldId: asBookingFieldId(fieldId) }),
1772
+ selectPaymentOption: (kind) => dispatch({ type: "payment-option-selected", kind }),
1773
+ selectAppointmentPackage: (packageId) => dispatch({
1774
+ type: "appointment-package-selected",
1775
+ packageId: packageId ? asCustomerPassId(packageId) : null
1776
+ }),
1777
+ setDiscountCode: (code) => {
1778
+ dispatch({ type: "discount-code-changed", code });
1779
+ latestRef.current.setDiscountCode(code);
1780
+ },
1781
+ clearDiscountCode: () => {
1782
+ dispatch({ type: "discount-code-changed", code: "" });
1783
+ latestRef.current.setDiscountCode("");
1784
+ },
1785
+ setAltchaSolution: (solution) => dispatch({ type: "altcha-solution-set", solution }),
1786
+ invalidateSpamProtection: () => dispatch({ type: "altcha-solution-invalidated" }),
1787
+ next: async () => {
1788
+ const latest = latestRef.current;
1789
+ if (!latest.currentStep) return { ok: true };
1790
+ const validation = latest.validations.get(latest.currentStep.id) ?? {
1791
+ ok: true
1792
+ };
1793
+ if (!validation.ok) {
1794
+ dispatch({
1795
+ type: "validation-errors-set",
1796
+ errors: validation.errors
1797
+ });
1798
+ return {
1799
+ ok: false,
1800
+ reason: "validation-failed",
1801
+ errors: validation.errors
1802
+ };
1803
+ }
1804
+ dispatch({
1805
+ type: "validation-errors-set",
1806
+ errors: createEmptyBookingValidationErrors()
1807
+ });
1808
+ const nextStepId = latest.stepIds[latest.currentStepIndex + 1];
1809
+ if (nextStepId) dispatch({ type: "step-changed", stepId: nextStepId });
1810
+ return { ok: true };
1811
+ },
1812
+ back: () => {
1813
+ const latest = latestRef.current;
1814
+ const previousStepId = latest.stepIds[latest.currentStepIndex - 1];
1815
+ if (previousStepId) {
1816
+ dispatch({ type: "step-changed", stepId: previousStepId });
1817
+ }
1818
+ },
1819
+ goToStep: async (stepId) => {
1820
+ if (!latestRef.current.stepIds.includes(stepId)) {
1821
+ return { ok: false, reason: "blocked", message: "step-hidden" };
1822
+ }
1823
+ dispatch({ type: "step-changed", stepId });
1824
+ return { ok: true };
1825
+ },
1826
+ submit: submitBooking,
1827
+ reset: () => dispatch({ type: "reset", initial: latestRef.current.initialState }),
1828
+ clearFeedback: () => {
1829
+ dispatch({ type: "feedback-cleared" });
1830
+ latestRef.current.clearFeedback();
1831
+ }
1832
+ }),
1833
+ [latestRef, submitBooking]
1834
+ );
1835
+ const paymentResolution = useMemo(
1836
+ () => resolvePaymentMode({
1837
+ data,
1838
+ defaultServiceId,
1839
+ services: options.services
1840
+ }),
1841
+ [data, defaultServiceId, options.services]
1842
+ );
1843
+ const paymentSelection = useMemo(
1844
+ () => selectedService && paymentResolution.mode !== "unsupported" ? buildBookingPaymentSelectionViewModel({
1845
+ serviceTitle: selectedService.title,
1846
+ paymentTerms: selectedService.paymentTerms,
1847
+ priceAmount: selectedService.priceAmount,
1848
+ priceCurrency: selectedService.priceCurrency,
1849
+ bookingStartAt: state.selectedSlot?.startAt
1850
+ }) : null,
1851
+ [paymentResolution.mode, selectedService, state.selectedSlot?.startAt]
1852
+ );
1853
+ const appointmentPackageItems = appointmentPackages.tag === "loaded" ? appointmentPackages.value : [];
1854
+ const selectedAppointmentPackage = appointmentPackageItems.find(
1855
+ (appointmentPackage) => appointmentPackage.customerPassId === state.selectedAppointmentPackageId
1856
+ ) ?? null;
1857
+ const spamProtectionEnabled = options.spamProtectionEnabled ?? isSpamProtectionEnabled();
1858
+ const status = resolveHeadlessBookingStatus({
1859
+ feedback: state.feedback,
1860
+ phase: state.phase
1861
+ });
1862
+ const view = resolveHeadlessBookingView({
1863
+ form: options.form,
1864
+ feedback: state.feedback
1865
+ });
1866
+ const appointmentPackagesModel = resolveAppointmentPackagesModel({
1867
+ enabled: Boolean(options.enableAppointmentPackages),
1868
+ canLoad: canLoadAppointmentPackages,
1869
+ unavailableReason: appointmentPackagesUnavailableReason,
1870
+ state: appointmentPackages,
1871
+ items: appointmentPackageItems,
1872
+ selected: selectedAppointmentPackage
1873
+ });
1874
+ const payment = paymentResolution.mode === "unsupported" ? {
1875
+ kind: "unsupported",
1876
+ mode: "unsupported",
1877
+ selection: null,
1878
+ selectedCollectionOptionKind: state.selectedCollectionOptionKind,
1879
+ selectedAppointmentPackageId: state.selectedAppointmentPackageId,
1880
+ appointmentPackages: appointmentPackagesModel,
1881
+ errorMessage: paymentResolution.errorMessage ?? "This service cannot be booked right now."
1882
+ } : {
1883
+ kind: "ok",
1884
+ mode: paymentResolution.mode,
1885
+ selection: paymentSelection,
1886
+ selectedCollectionOptionKind: state.selectedCollectionOptionKind,
1887
+ selectedAppointmentPackageId: state.selectedAppointmentPackageId,
1888
+ appointmentPackages: appointmentPackagesModel
1889
+ };
1890
+ return {
1891
+ status,
1892
+ view,
1893
+ steps: stepState.steps,
1894
+ currentStep: stepState.currentStep,
1895
+ data,
1896
+ services: {
1897
+ items: options.services,
1898
+ selected: selectedService,
1899
+ defaultServiceId,
1900
+ requiresSelection: Boolean(options.requiresServiceSelection)
1901
+ },
1902
+ resources: {
1903
+ items: resources,
1904
+ selected: resources.find((resource) => resource.id === selectedResourceId) ?? null,
1905
+ selectedResourceId,
1906
+ state: resourcesState,
1907
+ requiresSelection: Boolean(options.requiresResourceSelection)
1908
+ },
1909
+ availability: {
1910
+ selectedDate: state.selectedDate,
1911
+ selectedSlot: state.selectedSlot,
1912
+ availableDates: dateValues,
1913
+ slots: slotViews,
1914
+ datesState: datesError ? { tag: "error", message: datesError } : isLoadingDates ? { tag: "loading" } : { tag: "loaded", value: dateValues },
1915
+ slotsState: slotsError ? { tag: "error", message: slotsError } : isLoadingSlots ? { tag: "loading" } : { tag: "loaded", value: slotViews },
1916
+ timezone: viewerTimeZone,
1917
+ hasMoreDates: hasMore,
1918
+ loadMoreDates: loadMore
1919
+ },
1920
+ fields: {
1921
+ items: fields,
1922
+ values: state.fieldValues,
1923
+ errors: state.validationErrors.fieldErrors,
1924
+ touched: state.touchedFields
1925
+ },
1926
+ payment,
1927
+ discount: {
1928
+ code: discountUi.discountCode,
1929
+ isOpen: discountUi.discountUiOpen,
1930
+ manualCodeEntryAllowed: discountUi.manualCodeEntryAllowed,
1931
+ loading: discountUi.discountsAvailableLoading,
1932
+ autoAppliedDiscount: discountUi.autoAppliedDiscount,
1933
+ setCode: actions.setDiscountCode,
1934
+ setOpen: discountUi.setDiscountUiOpen,
1935
+ clear: actions.clearDiscountCode
1936
+ },
1937
+ spamProtection: spamProtectionEnabled ? {
1938
+ enabled: true,
1939
+ provider: "altcha",
1940
+ challenge: { url: getChallengeUrl(), auto: "onfocus" },
1941
+ solution: state.altchaSolution
1942
+ } : { enabled: false },
1943
+ feedback: state.feedback,
1944
+ actions
1945
+ };
1946
+ }
1947
+ function navigateToCheckout(checkoutUrl) {
1948
+ window.location.href = checkoutUrl;
1949
+ }
1950
+ function resolvePaymentMode(input) {
1951
+ return resolveBookingPaymentStep(input);
1952
+ }
1953
+ function useLatestRef(value) {
1954
+ const ref = useRef(value);
1955
+ useLayoutEffect(() => {
1956
+ ref.current = value;
1957
+ }, [value]);
1958
+ return ref;
1959
+ }
1960
+ function useAppointmentCheckoutVerification(config) {
1961
+ const api = useBlockApi();
1962
+ const pollingConfig = useMemo(() => {
1963
+ if (!config) return null;
1964
+ return {
1965
+ pollFn: () => api.getAppointmentBookingStatus({ sessionId: config.sessionId }),
1966
+ isComplete: (result) => (result.paymentStatus === "completed" || result.paymentStatus === "not_required") && result.appointmentStatus === "confirmed",
1967
+ isFailed: (result) => result.paymentStatus === "failed" || result.paymentStatus === "cancelled" || result.paymentStatus === "refunded" || result.appointmentStatus === "cancelled",
1968
+ maxAttempts: config.maxAttempts,
1969
+ interval: config.pollInterval
1970
+ };
1971
+ }, [api, config]);
1972
+ const polling = usePolling(pollingConfig);
1973
+ const state = resolveAppointmentCheckoutVerificationState({
1974
+ config,
1975
+ pollingStatus: polling.status,
1976
+ result: polling.result,
1977
+ error: polling.error
1978
+ });
1979
+ return {
1980
+ state,
1981
+ retry: polling.retry
1982
+ };
1983
+ }
1984
+ var DatePicker = ({
1985
+ value,
1986
+ onChange,
1987
+ dateOptions,
1988
+ required = true,
1989
+ isLoading = false,
1990
+ isEmpty = false,
1991
+ hasMore = false,
1992
+ onLoadMore
1993
+ }) => {
1994
+ if (isLoading) {
1995
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
1996
+ /* @__PURE__ */ jsxs("label", { className: "ab-label", children: [
1997
+ "Select Date",
1998
+ required && /* @__PURE__ */ jsx("span", { className: "ab-required", children: "*" })
1999
+ ] }),
2000
+ /* @__PURE__ */ jsx("div", { className: "ab-skeleton" }),
2001
+ /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted ab-text--sm", children: "Loading available dates..." })
2002
+ ] });
2003
+ }
2004
+ if (isEmpty) {
2005
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
2006
+ /* @__PURE__ */ jsxs("label", { className: "ab-label", children: [
2007
+ "Select Date",
2008
+ required && /* @__PURE__ */ jsx("span", { className: "ab-required", children: "*" })
2009
+ ] }),
2010
+ /* @__PURE__ */ jsx(FeedbackMessage, { tone: "warning", className: "ab-alert ab-alert--warning", role: "status", children: /* @__PURE__ */ jsxs("p", { className: "ab-text", children: [
2011
+ "No available dates in this period.",
2012
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
2013
+ "button",
2014
+ {
2015
+ type: "button",
2016
+ onClick: onLoadMore,
2017
+ className: "ab-link",
2018
+ children: "Check later dates"
2019
+ }
2020
+ )
2021
+ ] }) })
2022
+ ] });
2023
+ }
2024
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
2025
+ /* @__PURE__ */ jsxs("label", { htmlFor: "booking-date", className: "ab-label", children: [
2026
+ "Select Date",
2027
+ required && /* @__PURE__ */ jsx("span", { className: "ab-required", children: "*" })
2028
+ ] }),
2029
+ /* @__PURE__ */ jsxs(
2030
+ "select",
2031
+ {
2032
+ id: "booking-date",
2033
+ value,
2034
+ onChange: (e) => onChange(e.target.value),
2035
+ required,
2036
+ className: "ab-select",
2037
+ children: [
2038
+ /* @__PURE__ */ jsx("option", { value: "", children: "Choose a date..." }),
2039
+ dateOptions.map((date) => /* @__PURE__ */ jsx("option", { value: date, children: formatDate(date) }, date))
2040
+ ]
2041
+ }
2042
+ ),
2043
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
2044
+ "button",
2045
+ {
2046
+ type: "button",
2047
+ onClick: onLoadMore,
2048
+ className: "ab-link",
2049
+ children: "Load more dates \u2192"
2050
+ }
2051
+ )
2052
+ ] });
2053
+ };
2054
+ var TimeSlotSelector = ({
2055
+ value,
2056
+ onChange,
2057
+ slots,
2058
+ isLoading,
2059
+ required = true
2060
+ }) => {
2061
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
2062
+ /* @__PURE__ */ jsxs("label", { htmlFor: "booking-slot", className: "ab-label", children: [
2063
+ "Select Time",
2064
+ required && /* @__PURE__ */ jsx("span", { className: "ab-required", children: "*" })
2065
+ ] }),
2066
+ isLoading ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info ab-center", role: "status", children: "Loading available times..." }) : slots.length === 0 ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info ab-center", role: "status", children: "No available times for this date" }) : /* @__PURE__ */ jsxs(
2067
+ "select",
2068
+ {
2069
+ id: "booking-slot",
2070
+ value,
2071
+ onChange: (e) => onChange(e.target.value),
2072
+ required,
2073
+ className: "ab-select",
2074
+ children: [
2075
+ /* @__PURE__ */ jsx("option", { value: "", children: "Choose a time..." }),
2076
+ slots.map((slot) => /* @__PURE__ */ jsx("option", { value: slot.startAt, children: formatTime(slot.startAt) }, slot.startAt))
2077
+ ]
2078
+ }
2079
+ )
2080
+ ] });
2081
+ };
2082
+ var paymentOptionLayoutStyle = {
2083
+ alignItems: "flex-start",
2084
+ display: "flex",
2085
+ flexDirection: "column",
2086
+ gap: "0.25rem",
2087
+ justifyContent: "flex-start",
2088
+ width: "100%"
2089
+ };
2090
+ var summaryStackStyle = {
2091
+ gap: "0.125rem"
2092
+ };
2093
+ function HeadlessBookingDefaultForm({
2094
+ booking,
2095
+ onSubmit
2096
+ }) {
2097
+ const currentStep = booking.currentStep;
2098
+ const currentStepIndex = currentStep?.visibleIndex ?? 0;
2099
+ const isFirstStep = currentStepIndex === 0;
2100
+ const isLastStep = currentStepIndex === booking.steps.length - 1;
2101
+ const isSubmitting = booking.status.tag === "submitting";
2102
+ const handlePrimaryAction = async () => {
2103
+ const validation = await booking.actions.next();
2104
+ if (!validation.ok) return;
2105
+ if (isLastStep) {
2106
+ await onSubmit();
2107
+ }
2108
+ };
2109
+ if (!currentStep) {
2110
+ return /* @__PURE__ */ jsx("div", { className: "rb-text-center rb-py-12", children: /* @__PURE__ */ jsx("p", { className: "status-muted", children: "No steps available" }) });
2111
+ }
2112
+ return /* @__PURE__ */ jsxs("div", { className: "multi-step-form", children: [
2113
+ /* @__PURE__ */ jsx("div", { className: "rb-mb-8", children: /* @__PURE__ */ jsx(
2114
+ DefaultBookingProgress,
2115
+ {
2116
+ steps: booking.steps,
2117
+ currentIndex: currentStepIndex
2118
+ }
2119
+ ) }),
2120
+ /* @__PURE__ */ jsx("div", { className: "step-content", children: /* @__PURE__ */ jsx(DefaultBookingStepContent, { booking }) }),
2121
+ /* @__PURE__ */ jsx(DefaultBookingErrorSummary, { errors: currentStep.errors }),
2122
+ /* @__PURE__ */ jsxs("div", { className: "rb-flex rb-gap-3 rb-mt-8", children: [
2123
+ !isFirstStep ? /* @__PURE__ */ jsx(
2124
+ "button",
2125
+ {
2126
+ type: "button",
2127
+ onClick: booking.actions.back,
2128
+ className: themeButtonClassName({ variant: "outline", size: "md" }),
2129
+ children: "Back"
2130
+ }
2131
+ ) : null,
2132
+ /* @__PURE__ */ jsx(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ onClick: () => {
2137
+ void handlePrimaryAction();
2138
+ },
2139
+ disabled: isSubmitting,
2140
+ className: themeButtonClassName({
2141
+ variant: "primary",
2142
+ size: "md",
2143
+ extraClassName: "rb-flex-1"
2144
+ }),
2145
+ children: isSubmitting ? /* @__PURE__ */ jsxs(Fragment, { children: [
2146
+ /* @__PURE__ */ jsx(
2147
+ SpinnerNode,
2148
+ {
2149
+ size: "sm",
2150
+ color: "inherit",
2151
+ className: "rb-mr-2",
2152
+ srLabel: "Submitting"
2153
+ }
2154
+ ),
2155
+ "Submitting..."
2156
+ ] }) : isLastStep ? "Complete" : "Continue"
2157
+ }
2158
+ )
2159
+ ] }),
2160
+ /* @__PURE__ */ jsx(DefaultBookingDiscount, { booking }),
2161
+ /* @__PURE__ */ jsx(DefaultBookingFeedback, { booking }),
2162
+ /* @__PURE__ */ jsx(DefaultBookingAltcha, { booking })
2163
+ ] });
2164
+ }
2165
+ function DefaultBookingProgress({
2166
+ steps,
2167
+ currentIndex
2168
+ }) {
2169
+ return /* @__PURE__ */ jsx("div", { className: "rb-flex rb-items-center rb-justify-between", children: steps.map((step, index) => {
2170
+ const isActive = index === currentIndex;
2171
+ const isCompleted = index < currentIndex;
2172
+ const isLast = index === steps.length - 1;
2173
+ return /* @__PURE__ */ jsxs("div", { className: "rb-flex rb-items-center rb-flex-1", children: [
2174
+ /* @__PURE__ */ jsxs("div", { className: "rb-flex rb-items-center rb-gap-2 rb-cursor-default", children: [
2175
+ /* @__PURE__ */ jsx(
2176
+ "div",
2177
+ {
2178
+ className: `
2179
+ step-circle transition-theme-standard
2180
+ ${isActive ? "step-circle-active" : ""}
2181
+ ${isCompleted ? "step-circle-complete" : ""}
2182
+ `,
2183
+ children: isCompleted ? "\u2713" : index + 1
2184
+ }
2185
+ ),
2186
+ /* @__PURE__ */ jsx(
2187
+ "span",
2188
+ {
2189
+ className: `
2190
+ rb-caption rb-hidden rb-sm-inline
2191
+ ${isCompleted || !isActive ? "status-muted" : ""}
2192
+ `,
2193
+ children: step.label
2194
+ }
2195
+ )
2196
+ ] }),
2197
+ !isLast ? /* @__PURE__ */ jsx("div", { className: "step-connector rb-ml-2 rb-mr-2 rb-hidden rb-sm-block" }) : null
2198
+ ] }, step.id);
2199
+ }) });
2200
+ }
2201
+ function DefaultBookingStepContent({
2202
+ booking
2203
+ }) {
2204
+ const stepId = booking.currentStep?.id;
2205
+ if (!stepId) return null;
2206
+ switch (stepId) {
2207
+ case "service-selection":
2208
+ return /* @__PURE__ */ jsx(DefaultServiceSelectionStep, { booking });
2209
+ case "datetime-selection":
2210
+ return /* @__PURE__ */ jsx(DefaultDateTimeSelectionStep, { booking });
2211
+ case "payment-selection":
2212
+ return /* @__PURE__ */ jsx(HeadlessBookingPaymentSelectionStep, { booking });
2213
+ case "custom-fields":
2214
+ return /* @__PURE__ */ jsx(DefaultDynamicFields, { fields: booking.fields.items, booking });
2215
+ default:
2216
+ return assertNever(stepId);
2217
+ }
2218
+ }
2219
+ function DefaultServiceSelectionStep({
2220
+ booking
2221
+ }) {
2222
+ const services = booking.services.items;
2223
+ const resources = booking.resources.items;
2224
+ const selectedServiceId = booking.services.selected?.id;
2225
+ const selectedResourceId = booking.resources.selectedResourceId;
2226
+ const showServiceSelect = booking.services.requiresSelection || services.length > 1;
2227
+ const showResourceSelect = booking.resources.requiresSelection || resources.length > 1;
2228
+ const allowAnyResource = !booking.resources.requiresSelection;
2229
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack", children: [
2230
+ /* @__PURE__ */ jsxs("div", { children: [
2231
+ /* @__PURE__ */ jsx("h2", { className: "ab-title", children: "Select Service" }),
2232
+ /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted", children: "Choose the service you'd like to book" })
2233
+ ] }),
2234
+ showServiceSelect ? /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
2235
+ /* @__PURE__ */ jsx("label", { className: "ab-subtitle", children: "Service" }),
2236
+ /* @__PURE__ */ jsx("div", { className: "ab-grid", children: services.map((service) => {
2237
+ const isSelected = selectedServiceId === service.id;
2238
+ return /* @__PURE__ */ jsx(
2239
+ "button",
2240
+ {
2241
+ type: "button",
2242
+ onClick: () => booking.actions.selectService(service.id),
2243
+ className: `ab-card ${isSelected ? "ab-card--selected" : ""}`,
2244
+ children: /* @__PURE__ */ jsxs("div", { className: "ab-row--between", children: [
2245
+ /* @__PURE__ */ jsxs("div", { children: [
2246
+ /* @__PURE__ */ jsx("div", { className: "ab-subtitle", children: service.title }),
2247
+ service.description ? /* @__PURE__ */ jsx("div", { className: "ab-text ab-text--muted", children: service.description }) : null,
2248
+ service.durationMinutes ? /* @__PURE__ */ jsxs("div", { className: "ab-text ab-text--muted", children: [
2249
+ service.durationMinutes,
2250
+ " minutes"
2251
+ ] }) : null
2252
+ ] }),
2253
+ isSelected ? /* @__PURE__ */ jsx(SelectedCheck, {}) : null
2254
+ ] })
2255
+ },
2256
+ service.id
2257
+ );
2258
+ }) })
2259
+ ] }) : null,
2260
+ booking.resources.state.tag === "loading" ? /* @__PURE__ */ jsxs("div", { className: "ab-center", children: [
2261
+ /* @__PURE__ */ jsx(SpinnerNode, { size: "md", srLabel: "Loading practitioners" }),
2262
+ /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted", children: "Loading practitioners..." })
2263
+ ] }) : null,
2264
+ booking.resources.state.tag === "error" ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: booking.resources.state.message }) : null,
2265
+ showResourceSelect && booking.resources.state.tag === "loaded" && resources.length === 0 ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: "No practitioners are available for this service right now." }) : null,
2266
+ showResourceSelect && booking.resources.state.tag !== "loading" && (allowAnyResource || resources.length > 0) ? /* @__PURE__ */ jsxs("div", { className: "ab-stack--sm", children: [
2267
+ /* @__PURE__ */ jsxs("label", { className: "ab-subtitle", children: [
2268
+ "Practitioner",
2269
+ booking.resources.requiresSelection ? "" : " (Optional)"
2270
+ ] }),
2271
+ /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted", children: allowAnyResource ? 'Choose a specific practitioner or select "Any Available"' : "Choose the practitioner you'd like to book" }),
2272
+ /* @__PURE__ */ jsxs("div", { className: "ab-grid", children: [
2273
+ allowAnyResource ? /* @__PURE__ */ jsx(
2274
+ "button",
2275
+ {
2276
+ type: "button",
2277
+ onClick: () => booking.actions.selectResource(null),
2278
+ className: `ab-card ${!selectedResourceId ? "ab-card--selected" : ""}`,
2279
+ children: /* @__PURE__ */ jsxs("div", { className: "ab-row--between", children: [
2280
+ /* @__PURE__ */ jsx("div", { className: "ab-subtitle", children: "Any Available" }),
2281
+ !selectedResourceId ? /* @__PURE__ */ jsx(SelectedCheck, {}) : null
2282
+ ] })
2283
+ }
2284
+ ) : null,
2285
+ resources.map((resource) => {
2286
+ const isSelected = selectedResourceId === resource.id;
2287
+ return /* @__PURE__ */ jsx(
2288
+ "button",
2289
+ {
2290
+ type: "button",
2291
+ onClick: () => booking.actions.selectResource(resource.id),
2292
+ className: `ab-card ${isSelected ? "ab-card--selected" : ""}`,
2293
+ children: /* @__PURE__ */ jsxs("div", { className: "ab-row--between", children: [
2294
+ /* @__PURE__ */ jsx("div", { className: "ab-subtitle", children: resource.label }),
2295
+ isSelected ? /* @__PURE__ */ jsx(SelectedCheck, {}) : null
2296
+ ] })
2297
+ },
2298
+ resource.id
2299
+ );
2300
+ })
2301
+ ] })
2302
+ ] }) : null
2303
+ ] });
2304
+ }
2305
+ function DefaultDateTimeSelectionStep({
2306
+ booking
2307
+ }) {
2308
+ const selectedDate = booking.availability.selectedDate ?? "";
2309
+ const selectedSlotStart = booking.availability.selectedSlot?.startAt ?? "";
2310
+ const singleService = booking.services.items.length === 1 ? booking.services.items[0] : void 0;
2311
+ const slots = collapseHeadlessSlotsForDisplay({
2312
+ slots: booking.availability.slots,
2313
+ hasSelectedResource: Boolean(booking.resources.selectedResourceId)
2314
+ });
2315
+ const error = booking.availability.datesState.tag === "error" ? booking.availability.datesState.message : booking.availability.slotsState.tag === "error" ? booking.availability.slotsState.message : null;
2316
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack", children: [
2317
+ singleService ? /* @__PURE__ */ jsxs("div", { className: "ab-info-box", children: [
2318
+ /* @__PURE__ */ jsx("h3", { className: "ab-subtitle", children: singleService.title }),
2319
+ singleService.description ? /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted", children: singleService.description }) : null,
2320
+ singleService.durationMinutes ? /* @__PURE__ */ jsxs("p", { className: "ab-text ab-text--muted", children: [
2321
+ "Duration: ",
2322
+ singleService.durationMinutes,
2323
+ " minutes"
2324
+ ] }) : null
2325
+ ] }) : null,
2326
+ error ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: error }) : null,
2327
+ /* @__PURE__ */ jsx(
2328
+ DatePicker,
2329
+ {
2330
+ value: selectedDate,
2331
+ onChange: (date) => booking.actions.selectDate(date || null),
2332
+ dateOptions: [...booking.availability.availableDates],
2333
+ isLoading: booking.availability.datesState.tag === "loading",
2334
+ isEmpty: booking.availability.datesState.tag !== "loading" && booking.availability.availableDates.length === 0,
2335
+ hasMore: booking.availability.hasMoreDates,
2336
+ onLoadMore: booking.availability.loadMoreDates
2337
+ }
2338
+ ),
2339
+ selectedDate ? /* @__PURE__ */ jsx(
2340
+ TimeSlotSelector,
2341
+ {
2342
+ value: selectedSlotStart,
2343
+ onChange: (slotStartTime) => {
2344
+ booking.actions.selectSlot(
2345
+ booking.availability.slots.find(
2346
+ (slot) => slot.startAt === slotStartTime
2347
+ ) ?? null
2348
+ );
2349
+ },
2350
+ slots,
2351
+ isLoading: booking.availability.slotsState.tag === "loading"
2352
+ }
2353
+ ) : null
2354
+ ] });
2355
+ }
2356
+ function HeadlessBookingPaymentSelectionStep({
2357
+ booking
2358
+ }) {
2359
+ const service = booking.services.selected;
2360
+ const viewModel = booking.payment.selection;
2361
+ if (booking.payment.kind === "unsupported") {
2362
+ return /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: booking.payment.errorMessage });
2363
+ }
2364
+ if (!service || !viewModel) {
2365
+ return /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: "Payment options are unavailable for this service right now." });
2366
+ }
2367
+ const packageOptions = booking.payment.appointmentPackages.items;
2368
+ const hasPackageOptions = packageOptions.length > 0;
2369
+ return /* @__PURE__ */ jsxs("div", { className: "ab-stack", children: [
2370
+ /* @__PURE__ */ jsxs("div", { className: "ab-info-box", children: [
2371
+ /* @__PURE__ */ jsx("h3", { className: "ab-subtitle", children: viewModel.sectionTitle }),
2372
+ /* @__PURE__ */ jsx("p", { className: "ab-text ab-text--muted", children: service.title })
2373
+ ] }),
2374
+ booking.payment.appointmentPackages.state.tag === "loading" ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info", role: "status", children: "Checking package credits..." }) : null,
2375
+ booking.payment.appointmentPackages.state.tag === "error" ? /* @__PURE__ */ jsxs(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info", role: "status", children: [
2376
+ booking.payment.appointmentPackages.state.message,
2377
+ " You can continue with the normal payment option."
2378
+ ] }) : null,
2379
+ /* @__PURE__ */ jsxs("div", { className: "ab-stack", role: "list", "aria-label": "Payment options", children: [
2380
+ packageOptions.map((appointmentPackage) => {
2381
+ const isSelected = booking.payment.selectedAppointmentPackageId === appointmentPackage.customerPassId;
2382
+ return /* @__PURE__ */ jsx(
2383
+ "button",
2384
+ {
2385
+ type: "button",
2386
+ className: themeButtonClassName({
2387
+ variant: isSelected ? "primary" : "outline",
2388
+ size: "md"
2389
+ }),
2390
+ style: paymentOptionLayoutStyle,
2391
+ onClick: () => booking.actions.selectAppointmentPackage(
2392
+ appointmentPackage.customerPassId
2393
+ ),
2394
+ "aria-pressed": isSelected,
2395
+ children: /* @__PURE__ */ jsx(
2396
+ PaymentOptionContent,
2397
+ {
2398
+ id: appointmentPackage.customerPassId,
2399
+ title: appointmentPackage.passName,
2400
+ description: "Use 1 package credit",
2401
+ summaryRows: [{
2402
+ label: "Balance",
2403
+ value: `${appointmentPackage.creditsRemaining} of ${appointmentPackage.creditsTotal} credits remaining`
2404
+ }]
2405
+ }
2406
+ )
2407
+ },
2408
+ appointmentPackage.customerPassId
2409
+ );
2410
+ }),
2411
+ viewModel.options.map((option) => {
2412
+ const isSelected = !booking.payment.selectedAppointmentPackageId && (booking.payment.selectedCollectionOptionKind === option.kind || viewModel.mode === "single_option");
2413
+ if (viewModel.mode === "single_option" && !hasPackageOptions) {
2414
+ return /* @__PURE__ */ jsx(
2415
+ "div",
2416
+ {
2417
+ className: "ab-card",
2418
+ style: paymentOptionLayoutStyle,
2419
+ children: /* @__PURE__ */ jsx(
2420
+ PaymentOptionContent,
2421
+ {
2422
+ id: option.kind,
2423
+ title: option.title,
2424
+ description: option.description,
2425
+ summaryRows: option.summaryRows,
2426
+ helperText: option.helperText
2427
+ }
2428
+ )
2429
+ },
2430
+ option.kind
2431
+ );
2432
+ }
2433
+ return /* @__PURE__ */ jsx(
2434
+ "button",
2435
+ {
2436
+ type: "button",
2437
+ className: themeButtonClassName({
2438
+ variant: isSelected ? "primary" : "outline",
2439
+ size: "md"
2440
+ }),
2441
+ style: paymentOptionLayoutStyle,
2442
+ onClick: () => booking.actions.selectPaymentOption(option.kind),
2443
+ "aria-pressed": isSelected,
2444
+ children: /* @__PURE__ */ jsx(
2445
+ PaymentOptionContent,
2446
+ {
2447
+ id: option.kind,
2448
+ title: option.title,
2449
+ description: option.description,
2450
+ summaryRows: option.summaryRows,
2451
+ helperText: option.helperText
2452
+ }
2453
+ )
2454
+ },
2455
+ option.kind
2456
+ );
2457
+ })
2458
+ ] })
2459
+ ] });
2460
+ }
2461
+ function PaymentOptionContent({
2462
+ description,
2463
+ helperText,
2464
+ id,
2465
+ summaryRows,
2466
+ title
2467
+ }) {
2468
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2469
+ /* @__PURE__ */ jsx("span", { children: title }),
2470
+ /* @__PURE__ */ jsx("span", { className: "ab-text ab-text--muted", children: description }),
2471
+ /* @__PURE__ */ jsx("div", { className: "ab-stack", style: summaryStackStyle, children: summaryRows.map((row) => /* @__PURE__ */ jsxs("span", { className: "ab-text ab-text--muted", children: [
2472
+ row.label,
2473
+ ": ",
2474
+ row.value
2475
+ ] }, `${id}-${row.label}`)) }),
2476
+ helperText ? /* @__PURE__ */ jsx("span", { className: "ab-text ab-text--muted", children: helperText }) : null
2477
+ ] });
2478
+ }
2479
+ function DefaultDynamicFields({
2480
+ booking,
2481
+ fields
2482
+ }) {
2483
+ return /* @__PURE__ */ jsx("div", { className: "rb-space-y-6", children: fields.map((field) => /* @__PURE__ */ jsx(DefaultDynamicField, { booking, field }, field.id)) });
2484
+ }
2485
+ function DefaultDynamicField({
2486
+ booking,
2487
+ field
2488
+ }) {
2489
+ const value = field.value ?? "";
2490
+ const error = field.error;
2491
+ const errorId = error ? field.errorId ?? `${field.inputId}-error` : null;
2492
+ const describedBy = [field.helpTextId, errorId].filter(Boolean).join(" ") || void 0;
2493
+ return /* @__PURE__ */ jsxs("div", { className: "rb-space-y-2", children: [
2494
+ /* @__PURE__ */ jsxs("label", { id: field.labelId, htmlFor: field.inputId, className: "form-label", children: [
2495
+ field.label,
2496
+ field.required ? /* @__PURE__ */ jsx("span", { className: "required-marker", children: "*" }) : null
2497
+ ] }),
2498
+ field.helpText ? /* @__PURE__ */ jsx("p", { id: field.helpTextId ?? void 0, className: "rb-caption status-muted", children: field.helpText }) : null,
2499
+ /* @__PURE__ */ jsx(
2500
+ DefaultFieldInput,
2501
+ {
2502
+ field,
2503
+ value,
2504
+ onChange: (nextValue) => booking.actions.updateField(field.id, nextValue),
2505
+ onBlur: () => booking.actions.markFieldTouched(field.id),
2506
+ error,
2507
+ errorId: errorId ?? `${field.inputId}-error`,
2508
+ describedBy
2509
+ }
2510
+ ),
2511
+ error ? /* @__PURE__ */ jsx(
2512
+ FieldValidationMessage,
2513
+ {
2514
+ id: errorId ?? `${field.inputId}-error`,
2515
+ className: "rb-caption status-error",
2516
+ children: error
2517
+ }
2518
+ ) : null
2519
+ ] });
2520
+ }
2521
+ function DefaultFieldInput({
2522
+ describedBy,
2523
+ error,
2524
+ errorId,
2525
+ field,
2526
+ onBlur,
2527
+ onChange,
2528
+ value
2529
+ }) {
2530
+ const strValue = typeof value === "string" ? value : String(value ?? "");
2531
+ const validationProps = getFieldValidationControlProps({
2532
+ message: error,
2533
+ messageId: errorId,
2534
+ describedBy
2535
+ });
2536
+ const commonProps = {
2537
+ id: field.inputId,
2538
+ name: field.id,
2539
+ onBlur,
2540
+ required: field.required,
2541
+ ...validationProps
2542
+ };
2543
+ switch (field.type) {
2544
+ case "textarea":
2545
+ return /* @__PURE__ */ jsx(
2546
+ "textarea",
2547
+ {
2548
+ ...commonProps,
2549
+ value: strValue,
2550
+ onChange: (event) => onChange(event.target.value),
2551
+ placeholder: field.field.placeholder,
2552
+ minLength: field.field.minLength,
2553
+ maxLength: field.field.maxLength,
2554
+ rows: 4,
2555
+ className: "form-textarea"
2556
+ }
2557
+ );
2558
+ case "select":
2559
+ if (field.field.multiple) {
2560
+ return /* @__PURE__ */ jsx(
2561
+ "select",
2562
+ {
2563
+ ...commonProps,
2564
+ value: Array.isArray(value) ? value : [],
2565
+ onChange: (event) => {
2566
+ onChange(Array.from(
2567
+ event.target.selectedOptions,
2568
+ (option) => option.value
2569
+ ));
2570
+ },
2571
+ multiple: true,
2572
+ className: "form-select rb-h-32",
2573
+ children: field.field.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
2574
+ }
2575
+ );
2576
+ }
2577
+ return /* @__PURE__ */ jsxs(
2578
+ "select",
2579
+ {
2580
+ ...commonProps,
2581
+ value: strValue,
2582
+ onChange: (event) => onChange(event.target.value),
2583
+ className: "form-select",
2584
+ children: [
2585
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select an option..." }),
2586
+ field.field.options?.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
2587
+ ]
2588
+ }
2589
+ );
2590
+ case "radio":
2591
+ return /* @__PURE__ */ jsx("div", { className: "rb-space-y-2", children: field.field.options?.map((option) => /* @__PURE__ */ jsxs("label", { className: "rb-flex rb-items-center rb-gap-2 rb-cursor-pointer", children: [
2592
+ /* @__PURE__ */ jsx(
2593
+ "input",
2594
+ {
2595
+ type: "radio",
2596
+ name: field.id,
2597
+ value: option.value,
2598
+ checked: value === option.value,
2599
+ onChange: (event) => onChange(event.target.value),
2600
+ onBlur,
2601
+ required: field.required,
2602
+ ...validationProps,
2603
+ className: "form-radio"
2604
+ }
2605
+ ),
2606
+ /* @__PURE__ */ jsx("span", { className: "rb-caption", children: option.label })
2607
+ ] }, option.value)) });
2608
+ case "checkbox":
2609
+ if (field.field.options && field.field.options.length > 1) {
2610
+ const checkedValues = Array.isArray(value) ? value : [];
2611
+ return /* @__PURE__ */ jsx("div", { className: "rb-space-y-2", children: field.field.options.map((option) => /* @__PURE__ */ jsxs("label", { className: "rb-flex rb-items-center rb-gap-2 rb-cursor-pointer", children: [
2612
+ /* @__PURE__ */ jsx(
2613
+ "input",
2614
+ {
2615
+ type: "checkbox",
2616
+ name: field.id,
2617
+ value: option.value,
2618
+ checked: checkedValues.includes(option.value),
2619
+ onChange: (event) => {
2620
+ onChange(
2621
+ event.target.checked ? [...checkedValues, option.value] : checkedValues.filter((item) => item !== option.value)
2622
+ );
2623
+ },
2624
+ onBlur,
2625
+ ...validationProps,
2626
+ className: "form-checkbox"
2627
+ }
2628
+ ),
2629
+ /* @__PURE__ */ jsx("span", { className: "rb-caption", children: option.label })
2630
+ ] }, option.value)) });
2631
+ }
2632
+ return /* @__PURE__ */ jsxs("label", { className: "rb-flex rb-items-center rb-gap-2 rb-cursor-pointer", children: [
2633
+ /* @__PURE__ */ jsx(
2634
+ "input",
2635
+ {
2636
+ type: "checkbox",
2637
+ id: field.inputId,
2638
+ name: field.id,
2639
+ checked: Boolean(value),
2640
+ onChange: (event) => onChange(event.target.checked),
2641
+ onBlur,
2642
+ required: field.required,
2643
+ ...validationProps,
2644
+ className: "form-checkbox"
2645
+ }
2646
+ ),
2647
+ /* @__PURE__ */ jsx("span", { className: "rb-caption", children: field.field.placeholder || field.label })
2648
+ ] });
2649
+ case "booking_requirement_consent":
2650
+ return /* @__PURE__ */ jsxs("label", { className: "rb-flex rb-items-start rb-gap-2 rb-cursor-pointer", children: [
2651
+ /* @__PURE__ */ jsx(
2652
+ "input",
2653
+ {
2654
+ type: "checkbox",
2655
+ id: field.inputId,
2656
+ name: field.id,
2657
+ checked: Boolean(value),
2658
+ onChange: (event) => onChange(event.target.checked),
2659
+ onBlur,
2660
+ required: field.required,
2661
+ ...validationProps,
2662
+ className: "form-checkbox rb-mt-0.5"
2663
+ }
2664
+ ),
2665
+ /* @__PURE__ */ jsx("span", { className: "rb-caption", children: field.field.placeholder || field.label })
2666
+ ] });
2667
+ case "number":
2668
+ return /* @__PURE__ */ jsx(
2669
+ "input",
2670
+ {
2671
+ ...commonProps,
2672
+ type: "number",
2673
+ value: strValue,
2674
+ onChange: (event) => onChange(event.target.value),
2675
+ placeholder: field.field.placeholder,
2676
+ min: field.field.min,
2677
+ max: field.field.max,
2678
+ className: "form-input"
2679
+ }
2680
+ );
2681
+ case "date":
2682
+ return /* @__PURE__ */ jsx(
2683
+ "input",
2684
+ {
2685
+ ...commonProps,
2686
+ type: "date",
2687
+ value: strValue,
2688
+ onChange: (event) => onChange(event.target.value),
2689
+ min: field.field.min ? String(field.field.min) : void 0,
2690
+ max: field.field.max ? String(field.field.max) : void 0,
2691
+ className: "form-input"
2692
+ }
2693
+ );
2694
+ case "time":
2695
+ return /* @__PURE__ */ jsx(
2696
+ "input",
2697
+ {
2698
+ ...commonProps,
2699
+ type: "time",
2700
+ value: strValue,
2701
+ onChange: (event) => onChange(event.target.value),
2702
+ className: "form-input"
2703
+ }
2704
+ );
2705
+ default:
2706
+ return /* @__PURE__ */ jsx(
2707
+ "input",
2708
+ {
2709
+ ...commonProps,
2710
+ type: field.type,
2711
+ value: strValue,
2712
+ onChange: (event) => onChange(event.target.value),
2713
+ placeholder: field.field.placeholder,
2714
+ minLength: field.field.minLength,
2715
+ maxLength: field.field.maxLength,
2716
+ pattern: field.field.pattern,
2717
+ className: "form-input"
2718
+ }
2719
+ );
2720
+ }
2721
+ }
2722
+ function DefaultBookingDiscount({
2723
+ booking
2724
+ }) {
2725
+ const shouldShowDiscountSection = !booking.payment.selectedAppointmentPackageId && !booking.discount.loading && (booking.discount.manualCodeEntryAllowed || Boolean(booking.discount.autoAppliedDiscount));
2726
+ if (!shouldShowDiscountSection) return null;
2727
+ return /* @__PURE__ */ jsxs("div", { className: "rb-mt-6", children: [
2728
+ booking.discount.manualCodeEntryAllowed ? /* @__PURE__ */ jsx(
2729
+ DiscountCodeSection,
2730
+ {
2731
+ isOpen: booking.discount.isOpen,
2732
+ code: booking.discount.code,
2733
+ onOpen: () => booking.discount.setOpen(true),
2734
+ onChange: booking.discount.setCode,
2735
+ onRemove: () => {
2736
+ booking.discount.clear();
2737
+ booking.discount.setOpen(false);
2738
+ }
2739
+ }
2740
+ ) : null,
2741
+ booking.discount.autoAppliedDiscount && booking.discount.code.trim().length === 0 ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info rb-mt-4", role: "status", children: /* @__PURE__ */ jsxs("p", { className: "ab-text", children: [
2742
+ "Discount available: ",
2743
+ booking.discount.autoAppliedDiscount.publicLabel,
2744
+ " ",
2745
+ "will apply automatically at checkout."
2746
+ ] }) }) : null
2747
+ ] });
2748
+ }
2749
+ function DefaultBookingFeedback({
2750
+ booking
2751
+ }) {
2752
+ if (booking.feedback.kind !== "error") return null;
2753
+ return /* @__PURE__ */ jsxs(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: [
2754
+ /* @__PURE__ */ jsx("p", { className: "ab-text", children: booking.feedback.message }),
2755
+ /* @__PURE__ */ jsx(
2756
+ "button",
2757
+ {
2758
+ type: "button",
2759
+ className: themeButtonClassName({ variant: "secondary", size: "sm" }),
2760
+ onClick: booking.actions.clearFeedback,
2761
+ children: "Dismiss"
2762
+ }
2763
+ )
2764
+ ] });
2765
+ }
2766
+ function DefaultBookingAltcha({
2767
+ booking
2768
+ }) {
2769
+ if (!booking.spamProtection.enabled) return null;
2770
+ return /* @__PURE__ */ jsx("div", { className: "ab-altcha", children: /* @__PURE__ */ jsx(
2771
+ AltchaWidget,
2772
+ {
2773
+ challengeUrl: booking.spamProtection.challenge.url,
2774
+ onSolutionChange: booking.actions.setAltchaSolution
2775
+ }
2776
+ ) });
2777
+ }
2778
+ function DefaultBookingErrorSummary({
2779
+ errors
2780
+ }) {
2781
+ if (!hasBookingValidationErrors(errors)) return null;
2782
+ return /* @__PURE__ */ jsxs(FeedbackMessage, { tone: "error", className: "rb-mt-6 alert alert-error", role: "alert", children: [
2783
+ errors.formError ? /* @__PURE__ */ jsx("p", { className: "rb-caption", children: /* @__PURE__ */ jsx("strong", { children: errors.formError }) }) : null,
2784
+ Object.entries(errors.fieldErrors).map(([key, message]) => /* @__PURE__ */ jsx("p", { className: "rb-caption", children: message }, key))
2785
+ ] });
2786
+ }
2787
+ function SelectedCheck() {
2788
+ return /* @__PURE__ */ jsx("div", { className: "ab-check", children: /* @__PURE__ */ jsx(
2789
+ "svg",
2790
+ {
2791
+ fill: "none",
2792
+ strokeLinecap: "round",
2793
+ strokeLinejoin: "round",
2794
+ strokeWidth: "2",
2795
+ viewBox: "0 0 24 24",
2796
+ stroke: "currentColor",
2797
+ children: /* @__PURE__ */ jsx("path", { d: "M5 13l4 4L19 7" })
2798
+ }
2799
+ ) });
2800
+ }
2801
+ function collapseHeadlessSlotsForDisplay(input) {
2802
+ const seenStartTimes = /* @__PURE__ */ new Set();
2803
+ const visibleSlots = [];
2804
+ for (const slot of input.slots) {
2805
+ if (!input.hasSelectedResource) {
2806
+ if (seenStartTimes.has(slot.startAt)) continue;
2807
+ seenStartTimes.add(slot.startAt);
2808
+ }
2809
+ visibleSlots.push({
2810
+ startAt: slot.startAt,
2811
+ endAt: slot.endAt,
2812
+ resourceId: asAppointmentResourceId(slot.resourceId)
2813
+ });
2814
+ }
2815
+ return visibleSlots;
2816
+ }
2817
+ var PORTAL_REBOOK_QUERY_PARAM = "rebook";
2818
+ var PORTAL_REBOOK_QUERY_VALUE = "1";
2819
+ var REBOOK_COMPLETION_MODE_VERIFIED_CHECKOUT = "verified_checkout";
2820
+ var REBOOK_COMPLETION_MODE_BYPASSED = "bypassed";
2821
+ function getBookingRedirectLocationSnapshot() {
2822
+ if (typeof window === "undefined") {
2823
+ return { search: "", hash: "" };
2824
+ }
2825
+ return {
2826
+ search: window.location.search,
2827
+ hash: window.location.hash
2828
+ };
2829
+ }
2830
+ function appendRebookFlag(url, enabled) {
2831
+ if (!enabled) {
2832
+ return url;
2833
+ }
2834
+ const nextUrl = new URL(url);
2835
+ nextUrl.searchParams.set(
2836
+ PORTAL_REBOOK_QUERY_PARAM,
2837
+ PORTAL_REBOOK_QUERY_VALUE
2838
+ );
2839
+ return nextUrl.toString();
2840
+ }
2841
+ var BookingFormClient = ({
2842
+ siteId: siteIdProp,
2843
+ render
2844
+ }) => {
2845
+ const siteId = resolveBookingSiteId(siteIdProp, null);
2846
+ const viewerTimeZone = getRuntimeTimeZone();
2847
+ const form = render.hydration.form;
2848
+ const services = render.hydration.services;
2849
+ const defaultServiceId = render.hydration.serviceId ?? resolveDefaultBookingServiceId(form, services);
2850
+ const initialServiceId = render.hydration.serviceId ?? defaultServiceId ?? void 0;
2851
+ const initialResourceId = render.hydration.resourceId ?? void 0;
2852
+ const isPortalRebook = render.hydration.rebookContext?.kind === "portal_rebook";
2853
+ const hasLoggedRebookCompletionRef = useRef(false);
2854
+ const [redirectState] = useState(
2855
+ () => getBookingRedirectStateFromLocation(getBookingRedirectLocationSnapshot())
2856
+ );
2857
+ const showSpamProtection = render.hydration.spamProtectionEnabled ?? isSpamProtectionEnabled();
2858
+ const api = useBlockApi();
2859
+ const hasPortalAuth = api.config.auth.type === "bearer" || api.config.authOverrides?.portal?.type === "bearer";
2860
+ const formId = render.hydration.formId ?? form?.id ?? "booking-form";
2861
+ const booking = useHeadlessBookingFlow({
2862
+ siteId,
2863
+ formId,
2864
+ form,
2865
+ services,
2866
+ initialServiceId,
2867
+ initialResourceId,
2868
+ initialData: render.hydration.initialData ?? void 0,
2869
+ requiresServiceSelection: render.hydration.requiresServiceSelection,
2870
+ requiresResourceSelection: render.hydration.requiresResourceSelection,
2871
+ shouldAutoSelectSingleResource: render.hydration.shouldAutoSelectSingleResource,
2872
+ enableAppointmentPackages: hasPortalAuth,
2873
+ spamProtectionEnabled: showSpamProtection,
2874
+ successMessage: render.display.successMessage ?? void 0,
2875
+ viewerTimeZone
2876
+ });
2877
+ React2.useEffect(() => {
2878
+ if (!siteId) {
2879
+ console.error("[BookingFormClient] ERROR: siteId is missing!");
2880
+ }
2881
+ if (!form) {
2882
+ console.warn("[BookingFormClient] Form data not loaded");
2883
+ }
2884
+ }, [siteId, form]);
2885
+ const bookingAnchorId = form ? `booking-form-${form.id}` : "booking-form";
2886
+ const verification = useAppointmentCheckoutVerification(
2887
+ redirectState.kind === "success" ? { sessionId: redirectState.sessionId } : null
2888
+ );
2889
+ const viewState = deriveBookingFormViewState({
2890
+ localSuccessResourceName: booking.view.kind === "success" ? booking.view.resourceName : void 0,
2891
+ redirectState,
2892
+ verificationState: verification.state
2893
+ });
2894
+ React2.useEffect(() => {
2895
+ if (viewState.kind === "error") {
2896
+ scrollToBlock(bookingAnchorId);
2897
+ }
2898
+ }, [
2899
+ bookingAnchorId,
2900
+ viewState.kind,
2901
+ viewState.kind === "error" ? viewState.message : null
2902
+ ]);
2903
+ React2.useEffect(() => {
2904
+ if (viewState.kind === "success" && redirectState.kind === "success") {
2905
+ scrollToBlock(bookingAnchorId);
2906
+ }
2907
+ }, [bookingAnchorId, redirectState.kind, viewState.kind]);
2908
+ React2.useEffect(() => {
2909
+ if (!isPortalRebook || viewState.kind !== "success") {
2910
+ return;
2911
+ }
2912
+ if (hasLoggedRebookCompletionRef.current) {
2913
+ return;
2914
+ }
2915
+ hasLoggedRebookCompletionRef.current = true;
2916
+ logger.info("public:appointments:rebook:completed", {
2917
+ siteId,
2918
+ formId: render.hydration.formId,
2919
+ serviceId: booking.services.selected?.id ?? initialServiceId ?? null,
2920
+ resourceId: initialResourceId ?? null,
2921
+ completionMode: redirectState.kind === "success" ? REBOOK_COMPLETION_MODE_VERIFIED_CHECKOUT : REBOOK_COMPLETION_MODE_BYPASSED
2922
+ });
2923
+ }, [
2924
+ initialResourceId,
2925
+ initialServiceId,
2926
+ isPortalRebook,
2927
+ redirectState.kind,
2928
+ render.hydration.formId,
2929
+ booking.services.selected?.id,
2930
+ siteId,
2931
+ viewState.kind
2932
+ ]);
2933
+ if (render.display.state === "missing" || !form || !render.hydration.formId) {
2934
+ return /* @__PURE__ */ jsx(
2935
+ FeedbackMessage,
2936
+ {
2937
+ tone: "error",
2938
+ className: `ab-alert ab-alert--error ${render.display.className ?? ""}`.trim(),
2939
+ role: "alert",
2940
+ children: /* @__PURE__ */ jsx("p", { className: "ab-text", children: render.display.message ?? "Booking form not found. Please check your configuration." })
2941
+ }
2942
+ );
2943
+ }
2944
+ const handleComplete = async () => {
2945
+ const { successUrl, cancelUrl } = buildCheckoutReturnUrls(
2946
+ "booking",
2947
+ bookingAnchorId
2948
+ );
2949
+ const result = await booking.actions.submit({
2950
+ redirect: "manual",
2951
+ successUrl: appendRebookFlag(successUrl, isPortalRebook),
2952
+ cancelUrl: appendRebookFlag(cancelUrl, isPortalRebook)
2953
+ });
2954
+ if (!result.ok) return;
2955
+ if (result.outcome.kind === "checkout_redirect") {
2956
+ window.location.href = result.outcome.checkoutUrl;
2957
+ }
2958
+ };
2959
+ if (viewState.kind === "success") {
2960
+ return /* @__PURE__ */ jsx(
2961
+ SuccessMessage,
2962
+ {
2963
+ message: getBookingSuccessMessage(render.display.successMessage),
2964
+ resourceName: viewState.resourceName,
2965
+ className: render.display.className ?? void 0
2966
+ }
2967
+ );
2968
+ }
2969
+ if (viewState.kind === "verifying") {
2970
+ return /* @__PURE__ */ jsx(
2971
+ "div",
2972
+ {
2973
+ id: bookingAnchorId,
2974
+ className: render.display.className ?? void 0,
2975
+ children: /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info", role: "status", children: /* @__PURE__ */ jsx("p", { className: "ab-text", children: viewState.message }) })
2976
+ }
2977
+ );
2978
+ }
2979
+ return /* @__PURE__ */ jsxs("div", { id: bookingAnchorId, className: render.display.className ?? void 0, children: [
2980
+ render.hydration.noticeMessage ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "info", className: "ab-alert ab-alert--info rb-mb-4", role: "status", children: /* @__PURE__ */ jsx("p", { className: "ab-text", children: render.hydration.noticeMessage }) }) : null,
2981
+ viewState.kind === "error" ? /* @__PURE__ */ jsx(FeedbackMessage, { tone: "error", className: "ab-alert ab-alert--error", role: "alert", children: /* @__PURE__ */ jsx("p", { className: "ab-text", children: viewState.message }) }) : null,
2982
+ /* @__PURE__ */ jsx(HeadlessBookingDefaultForm, { booking, onSubmit: handleComplete })
2983
+ ] });
2984
+ };
2985
+ var decode = defineIslandPropsDecoder("booking-form");
2986
+ var enhance = (root, props) => {
2987
+ const { apiBaseUrl, portalToken } = props;
2988
+ const formId = props.render.hydration.formId;
2989
+ if (!formId) {
2990
+ throw new Error("[sdk:islands] booking-form requires formId");
2991
+ }
2992
+ const siteId = props.siteId;
2993
+ if (!siteId) {
2994
+ throw new Error("[sdk:islands] booking-form requires siteId");
2995
+ }
2996
+ if (!apiBaseUrl) {
2997
+ throw new Error("[sdk:islands] booking-form requires apiBaseUrl");
2998
+ }
2999
+ const apiConfig = {
3000
+ baseUrl: apiBaseUrl,
3001
+ auth: { type: "none" },
3002
+ authOverrides: portalToken ? { portal: { type: "bearer", token: portalToken } } : void 0,
3003
+ siteId
3004
+ };
3005
+ return mountReactIsland(root, ({ root: reactRoot }) => {
3006
+ reactRoot.render(
3007
+ /* @__PURE__ */ jsx(BlockApiProvider, { config: apiConfig, children: /* @__PURE__ */ jsx(BookingFormClient, { ...props }) })
3008
+ );
3009
+ });
3010
+ };
3011
+
3012
+ export { decode, enhance };