@code-collective/booking-widget 1.0.9 → 1.0.12

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 (43) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +500 -388
  3. package/dist/booking-widget.css +1 -1
  4. package/dist/booking-widget.js +2415 -1745
  5. package/dist/booking-widget.min.css +1 -1
  6. package/dist/booking-widget.min.js +39 -23
  7. package/dist/booking-widget.umd.cjs +5 -3
  8. package/package.json +58 -55
  9. package/src/lib/BookingProvider.svelte +21 -0
  10. package/src/lib/CartBar.svelte +26 -41
  11. package/src/lib/CartBarView.svelte +78 -61
  12. package/src/lib/CartExpiryGuard.svelte +15 -9
  13. package/src/lib/CartOverview.svelte +30 -20
  14. package/src/lib/CartOverviewButton.svelte +104 -101
  15. package/src/lib/Checkout.svelte +141 -128
  16. package/src/lib/CheckoutModal.svelte +838 -805
  17. package/src/lib/CheckoutPanel.svelte +121 -0
  18. package/src/lib/PaymentPage.svelte +191 -177
  19. package/src/lib/PickupPointPicker.svelte +1 -1
  20. package/src/lib/TicketConfigurator.svelte +166 -152
  21. package/src/lib/UnitCounter.svelte +16 -2
  22. package/src/lib/WizardPage.svelte +102 -35
  23. package/src/lib/app.css +0 -6
  24. package/src/lib/booking-context.ts +33 -0
  25. package/src/lib/cart-overview.svelte.ts +97 -0
  26. package/src/lib/config.ts +162 -153
  27. package/src/lib/elements/bw-cart.svelte +49 -35
  28. package/src/lib/elements/bw-checkout.svelte +97 -97
  29. package/src/lib/elements/bw-configurator.svelte +72 -56
  30. package/src/lib/elements/register.ts +171 -196
  31. package/src/lib/elements/shared.ts +18 -14
  32. package/src/lib/elements/theme.css +0 -6
  33. package/src/lib/host.svelte.ts +336 -0
  34. package/src/lib/index.ts +242 -196
  35. package/src/lib/layout.svelte.ts +52 -0
  36. package/src/lib/messages.ts +157 -77
  37. package/src/lib/peach-sdk.ts +86 -40
  38. package/src/lib/portal.ts +23 -0
  39. package/src/lib/CartExpiryGuard.test.ts +0 -331
  40. package/src/lib/CheckoutModal.confirm-outcome.test.ts +0 -91
  41. package/src/lib/CheckoutModal.payment-timeout.test.ts +0 -140
  42. package/src/lib/test/fixtures.ts +0 -107
  43. package/src/lib/test/messages-mock.ts +0 -34
@@ -1,196 +1,171 @@
1
- import '../app.css';
2
- import type { BookingApi } from '../api';
3
- import { ApiClient } from '../api';
4
- import { SessionManager } from '../session-manager';
5
- import { CartManager } from '../cart-manager';
6
- import { defaultApiBaseUrl } from './env';
7
- import { onWidgetMessage } from '../messages';
8
- import { mount } from 'svelte';
9
- import CartExpiryGuard from '../CartExpiryGuard.svelte';
10
-
11
- // Bootstrap shared services BEFORE element imports trigger connectedCallback.
12
- // Elements read from window.__bwServices instead of importing shared.ts,
13
- // because the IIFE bundler inlines imports per-element — a module-level
14
- // singleton would be duplicated. window is the only truly shared scope.
15
- const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
16
- const api: BookingApi = new ApiClient(defaultApiBaseUrl);
17
- const sessionManager = new SessionManager(api);
18
- const cartManager = new CartManager(api);
19
- sessionManager.startBackgroundRefresh();
20
- const ready = sessionManager.ensureSession(checkoutKey).then(() => {});
21
- (window as any).__bwServices = { api, cartManager, ready };
22
-
23
- function whenDomReady(fn: () => void) {
24
- if (document.readyState === 'loading') {
25
- document.addEventListener('DOMContentLoaded', fn, { once: true });
26
- } else {
27
- fn();
28
- }
29
- }
30
-
31
- // Mounted once, directly on <body> - not inside any <bw-*> element's own tree, so the "still shopping?"
32
- // warning and the expiry it can lead to work regardless of which combination of elements (if any include
33
- // <bw-checkout> at all) the merchant actually placed on this page. Session readiness is irrelevant to it -
34
- // cart operations only need the cart token, never the session token - so it does not wait on `ready`.
35
- //
36
- // Deferred to DOMContentLoaded rather than run inline: a <script> placed in <head> without defer/type=module
37
- // runs before <body> exists at all, and document.body would be null here. An exception at module level would
38
- // also skip the cart:updated forwarding and autoWire below, not just the expiry guard.
39
- function mountExpiryGuard() {
40
- const guardHost = document.createElement('div');
41
- guardHost.dataset.bwExpiryGuard = '';
42
- document.body.appendChild(guardHost);
43
- mount(CartExpiryGuard, { target: guardHost, props: { api, cartManager } });
44
- }
45
- whenDomReady(mountExpiryGuard);
46
-
47
- import './bw-configurator.svelte';
48
- import './bw-cart.svelte';
49
- import './bw-checkout.svelte';
50
-
51
- // Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire below),
52
- // so this is wired once here rather than per bw-configurator/bw-cart/bw-checkout instance. Carries the same
53
- // cart TicketConfigurator/CheckoutModal already fetch for their own posting - see those files' own comments -
54
- // so a consumer can build a custom cart summary (item count, remaining time, item details) without calling
55
- // the checkout API directly.
56
- onWidgetMessage((d) => {
57
- if (d.type === 'cart:updated' && 'cart' in d) {
58
- window.dispatchEvent(new CustomEvent('bw:cart-updated', { detail: { cart: d.cart } }));
59
- }
60
- if (d.type === 'cart:expired') {
61
- window.dispatchEvent(new CustomEvent('bw:cart-expired'));
62
- }
63
- });
64
-
65
- interface BwOptions {
66
- shouldBottomCloseOnModal: boolean;
67
- autoSelectSingleTimeSlot: boolean;
68
- wizardPages: string;
69
- editPages: string;
70
- }
71
-
72
- const defaultOptions: BwOptions = {
73
- shouldBottomCloseOnModal: true,
74
- autoSelectSingleTimeSlot: false,
75
- wizardPages: '',
76
- editPages: '',
77
- };
78
-
79
- const options: BwOptions = {
80
- ...defaultOptions,
81
- ...((window as any)['bwOptions'] ?? {}),
82
- };
83
-
84
- function forwardToWindow(e: Event) {
85
- if (e instanceof CustomEvent) {
86
- window.dispatchEvent(new CustomEvent(e.type, { detail: e.detail }));
87
- }
88
- }
89
-
90
- function autoWire() {
91
- const configurators = document.querySelectorAll('bw-configurator');
92
- const carts = document.querySelectorAll('bw-cart');
93
- const checkouts = document.querySelectorAll('bw-checkout');
94
-
95
- const allElements = [...configurators, ...carts, ...checkouts];
96
-
97
- // Apply options as element attributes
98
- if (options.wizardPages) {
99
- allElements.forEach((el) => {
100
- if (!el.hasAttribute('wizard-pages')) {
101
- el.setAttribute('wizard-pages', options.wizardPages);
102
- }
103
- });
104
- }
105
-
106
- // edit-pages only affects the checkout edit-item accordion, so it's only meaningful on bw-checkout.
107
- if (options.editPages) {
108
- checkouts.forEach((el) => {
109
- if (!el.hasAttribute('edit-pages')) {
110
- el.setAttribute('edit-pages', options.editPages);
111
- }
112
- });
113
- }
114
-
115
- [...configurators, ...checkouts].forEach((el) => {
116
- if (options.autoSelectSingleTimeSlot && !el.hasAttribute('auto-select-single-time-slot')) {
117
- el.setAttribute('auto-select-single-time-slot', '');
118
- }
119
- });
120
-
121
- // Auto-add is-modal to any checkout that doesn't already have it
122
- checkouts.forEach((el) => {
123
- if (!el.hasAttribute('is-modal') && !el.hasAttribute('is-inline')) {
124
- el.setAttribute('is-modal', '');
125
- }
126
- });
127
-
128
- function openCheckout() {
129
- checkouts.forEach((el) => {
130
- el.dispatchEvent(new CustomEvent('bw:open'));
131
- });
132
- window.dispatchEvent(new CustomEvent('bw:modal-open'));
133
-
134
- if (options.shouldBottomCloseOnModal) {
135
- hideCartBars();
136
- }
137
- }
138
-
139
- function showCarts() {
140
- carts.forEach((el) => {
141
- const parent = el.parentElement;
142
- if (parent) parent.classList.remove('hidden');
143
- (el as HTMLElement).style.display = '';
144
- });
145
- }
146
-
147
- function hideCartBars() {
148
- carts.forEach((el) => {
149
- if (el.getAttribute('display') === 'bar') {
150
- const parent = el.parentElement;
151
- if (parent) parent.classList.add('hidden');
152
- }
153
- });
154
- }
155
-
156
- function showCartBars() {
157
- carts.forEach((el) => {
158
- if (el.getAttribute('display') === 'bar') {
159
- const parent = el.parentElement;
160
- if (parent) parent.classList.remove('hidden');
161
- (el as HTMLElement).style.display = '';
162
- }
163
- });
164
- }
165
-
166
- // Forward bw:* events to window and wire up default behaviour
167
- configurators.forEach((el) => {
168
- el.addEventListener('bw:cancel', forwardToWindow);
169
- el.addEventListener('bw:cart-change', (e) => {
170
- forwardToWindow(e);
171
- showCarts();
172
- if (!el.hasAttribute('no-auto-checkout')) openCheckout();
173
-
174
- const handler = el.getAttribute('on-cart-change');
175
- if (handler && typeof (window as any)[handler] === 'function') {
176
- (window as any)[handler]();
177
- }
178
- });
179
- });
180
- carts.forEach((el) => {
181
- el.addEventListener('bw:checkout', (e) => {
182
- forwardToWindow(e);
183
- openCheckout();
184
- });
185
- });
186
- checkouts.forEach((el) => {
187
- el.addEventListener('bw:close', (e) => {
188
- forwardToWindow(e);
189
- window.dispatchEvent(new CustomEvent('bw:modal-close'));
190
- if (options.shouldBottomCloseOnModal) showCartBars();
191
- });
192
- el.addEventListener('bw:order-confirmed', forwardToWindow);
193
- });
194
- }
195
-
196
- whenDomReady(autoWire);
1
+ // The custom-elements build's DOM adapter. Everything it used to decide for itself - when checkout opens,
2
+ // where the expiry guard lives, what the site-wide defaults are - now lives in host.svelte.ts, which the ES
3
+ // build reaches too. What is left here is genuinely DOM: read attributes and window.bwOptions, and bind
4
+ // bw:* events to the host.
5
+
6
+ import '../app.css';
7
+ import { createBookingHost, setBookingDefaults } from '../host.svelte';
8
+ import { defaultApiBaseUrl } from './env';
9
+
10
+ interface BwOptions {
11
+ shouldBottomCloseOnModal: boolean;
12
+ autoSelectSingleTimeSlot: boolean;
13
+ autoOpenCheckout: boolean;
14
+ wizardPages: string;
15
+ editPages: string;
16
+ }
17
+
18
+ const defaultOptions: BwOptions = {
19
+ shouldBottomCloseOnModal: true,
20
+ autoSelectSingleTimeSlot: false,
21
+ autoOpenCheckout: true,
22
+ wizardPages: '',
23
+ editPages: '',
24
+ };
25
+
26
+ const options: BwOptions = {
27
+ ...defaultOptions,
28
+ ...((window as any)['bwOptions'] ?? {}),
29
+ };
30
+
31
+ // Bootstrap the host BEFORE element imports trigger connectedCallback. Elements read from
32
+ // window.__bwServices instead of importing shared.ts, because the IIFE bundler inlines imports per-element -
33
+ // a module-level singleton would be duplicated. window is the only truly shared scope.
34
+ const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
35
+
36
+ // Same values the host is built with, so an ES consumer sharing this page (an island alongside the elements)
37
+ // creates hosts that behave identically without restating them.
38
+ setBookingDefaults({
39
+ apiBaseUrl: defaultApiBaseUrl,
40
+ checkoutKey,
41
+ autoOpenCheckout: options.autoOpenCheckout,
42
+ shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
43
+ autoSelectSingleTimeSlot: options.autoSelectSingleTimeSlot,
44
+ });
45
+
46
+ const host = createBookingHost({
47
+ apiBaseUrl: defaultApiBaseUrl,
48
+ checkoutKey,
49
+ autoOpenCheckout: options.autoOpenCheckout,
50
+ shouldBottomCloseOnModal: options.shouldBottomCloseOnModal,
51
+ // Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire
52
+ // below). Carries the same cart TicketConfigurator/CheckoutModal already fetch for their own use, so a
53
+ // consumer can build a custom cart summary without calling the checkout API directly.
54
+ onCartUpdated: (cart) => dispatchOnWindow('bw:cart-updated', { cart }),
55
+ onCartExpired: () => dispatchOnWindow('bw:cart-expired', {}),
56
+ onCheckoutOpenChange: onCheckoutOpenChange,
57
+ });
58
+
59
+ (window as any).__bwServices = {
60
+ api: host.api,
61
+ cartManager: host.cartManager,
62
+ ready: host.ready,
63
+ host,
64
+ };
65
+
66
+ function dispatchOnWindow(name: string, detail: unknown) {
67
+ window.dispatchEvent(new CustomEvent(name, { detail }));
68
+ }
69
+
70
+ function whenDomReady(fn: () => void) {
71
+ if (document.readyState === 'loading') {
72
+ document.addEventListener('DOMContentLoaded', fn, { once: true });
73
+ } else {
74
+ fn();
75
+ }
76
+ }
77
+
78
+ import './bw-configurator.svelte';
79
+ import './bw-cart.svelte';
80
+ import './bw-checkout.svelte';
81
+
82
+ function cartElements(): HTMLElement[] {
83
+ return [...document.querySelectorAll<HTMLElement>('bw-cart')];
84
+ }
85
+
86
+ function setCartBarsHidden(hidden: boolean) {
87
+ cartElements().forEach((el) => {
88
+ if (el.getAttribute('display') !== 'bar') return;
89
+ el.parentElement?.classList.toggle('hidden', hidden);
90
+ if (!hidden) el.style.display = '';
91
+ });
92
+ }
93
+
94
+ // Called on the first add, to reveal carts a merchant left hidden until there was something in them. The
95
+ // host subscribes to the message bus before any element does, so by the time this runs it has already opened
96
+ // checkout for this same add and hidden the bottom bars - revealing everything unconditionally would put a
97
+ // bar straight back over the modal, which is the one thing shouldBottomCloseOnModal exists to prevent.
98
+ function showCarts() {
99
+ cartElements().forEach((el) => {
100
+ el.parentElement?.classList.remove('hidden');
101
+ el.style.display = '';
102
+ });
103
+ if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(host.isCheckoutOpen);
104
+ }
105
+
106
+ // The host owns whether checkout is open; bw-checkout renders from the same state. All this has to do is
107
+ // tell the page about it.
108
+ function onCheckoutOpenChange(open: boolean) {
109
+ dispatchOnWindow(open ? 'bw:modal-open' : 'bw:modal-close', {});
110
+ if (host.options.shouldBottomCloseOnModal) setCartBarsHidden(open);
111
+ }
112
+
113
+ function autoWire() {
114
+ const configurators = document.querySelectorAll('bw-configurator');
115
+ const carts = document.querySelectorAll('bw-cart');
116
+ const checkouts = document.querySelectorAll('bw-checkout');
117
+
118
+ const allElements = [...configurators, ...carts, ...checkouts];
119
+
120
+ // Apply options as element attributes
121
+ if (options.wizardPages) {
122
+ allElements.forEach((el) => {
123
+ if (!el.hasAttribute('wizard-pages')) {
124
+ el.setAttribute('wizard-pages', options.wizardPages);
125
+ }
126
+ });
127
+ }
128
+
129
+ // edit-pages only affects the checkout edit-item accordion, so it's only meaningful on bw-checkout.
130
+ if (options.editPages) {
131
+ checkouts.forEach((el) => {
132
+ if (!el.hasAttribute('edit-pages')) {
133
+ el.setAttribute('edit-pages', options.editPages);
134
+ }
135
+ });
136
+ }
137
+
138
+ [...configurators, ...checkouts].forEach((el) => {
139
+ if (options.autoSelectSingleTimeSlot && !el.hasAttribute('auto-select-single-time-slot')) {
140
+ el.setAttribute('auto-select-single-time-slot', '');
141
+ }
142
+ });
143
+
144
+ // Auto-add is-modal to any checkout that doesn't already have it
145
+ checkouts.forEach((el) => {
146
+ if (!el.hasAttribute('is-modal') && !el.hasAttribute('is-inline')) {
147
+ el.setAttribute('is-modal', '');
148
+ }
149
+ });
150
+
151
+ // Nothing here re-dispatches bw:* on window any more. Every one of them is dispatched on its element with
152
+ // bubbles+composed, so it reaches window by itself; forwarding a copy as well delivered each event twice,
153
+ // and a host counting bw:order-confirmed was double-counting orders. Bubbling is also the delivery that
154
+ // keeps working for an element added to the page after this ran, which a listener bound here would miss.
155
+ //
156
+ // Opening checkout is not wired here either: the add posts cart:change with its own openCheckout, the cart
157
+ // bar's button posts modal:open, and the host acts on both - so no-auto-checkout suppresses the open
158
+ // through the same branch an ES consumer's autoOpenCheckout: false does.
159
+ configurators.forEach((el) => {
160
+ el.addEventListener('bw:cart-change', () => {
161
+ showCarts();
162
+
163
+ const handler = el.getAttribute('on-cart-change');
164
+ if (handler && typeof (window as any)[handler] === 'function') {
165
+ (window as any)[handler]();
166
+ }
167
+ });
168
+ });
169
+ }
170
+
171
+ whenDomReady(autoWire);
@@ -1,14 +1,18 @@
1
- import type { BookingApi } from '../api';
2
- import type { CartManager } from '../cart-manager';
3
-
4
- interface SharedServices {
5
- api: BookingApi;
6
- cartManager: CartManager;
7
- ready: Promise<void>;
8
- }
9
-
10
- const w = window as any;
11
-
12
- export function getSharedServices(): SharedServices {
13
- return w.__bwServices;
14
- }
1
+ import type { BookingApi } from '../api';
2
+ import type { CartManager } from '../cart-manager';
3
+ import type { BookingHost } from '../host.svelte';
4
+
5
+ interface SharedServices {
6
+ api: BookingApi;
7
+ cartManager: CartManager;
8
+ ready: Promise<void>;
9
+ // The one host register.ts built. Elements read their behaviour from it rather than keeping their own -
10
+ // notably whether checkout is open, which used to live as separate state inside bw-checkout.
11
+ host: BookingHost;
12
+ }
13
+
14
+ const w = window as any;
15
+
16
+ export function getSharedServices(): SharedServices {
17
+ return w.__bwServices;
18
+ }
@@ -138,14 +138,8 @@ input, select, textarea { font-family: inherit; }
138
138
  height: 48px;
139
139
  font-size: 16px;
140
140
  font-weight: 700;
141
- position: relative;
142
141
  }
143
142
 
144
- .action-bar .btn-primary .arrow {
145
- position: absolute;
146
- right: 16px;
147
- font-size: 20px;
148
- }
149
143
 
150
144
  /* Buttons */
151
145
  .btn {