@code-collective/booking-widget 1.0.10 → 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.
- package/CHANGELOG.md +46 -0
- package/README.md +500 -388
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +2415 -1745
- package/dist/booking-widget.min.css +1 -1
- package/dist/booking-widget.min.js +39 -23
- package/dist/booking-widget.umd.cjs +5 -3
- package/package.json +58 -55
- package/src/lib/BookingProvider.svelte +21 -0
- package/src/lib/CartBar.svelte +26 -41
- package/src/lib/CartBarView.svelte +78 -61
- package/src/lib/CartExpiryGuard.svelte +15 -9
- package/src/lib/CartOverview.svelte +30 -20
- package/src/lib/CartOverviewButton.svelte +104 -101
- package/src/lib/Checkout.svelte +141 -128
- package/src/lib/CheckoutModal.svelte +838 -805
- package/src/lib/CheckoutPanel.svelte +121 -0
- package/src/lib/PaymentPage.svelte +191 -177
- package/src/lib/PickupPointPicker.svelte +1 -1
- package/src/lib/TicketConfigurator.svelte +166 -152
- package/src/lib/UnitCounter.svelte +16 -2
- package/src/lib/WizardPage.svelte +102 -35
- package/src/lib/app.css +0 -6
- package/src/lib/booking-context.ts +33 -0
- package/src/lib/cart-overview.svelte.ts +97 -0
- package/src/lib/config.ts +162 -153
- package/src/lib/elements/bw-cart.svelte +49 -35
- package/src/lib/elements/bw-checkout.svelte +97 -97
- package/src/lib/elements/bw-configurator.svelte +72 -56
- package/src/lib/elements/register.ts +171 -196
- package/src/lib/elements/shared.ts +18 -14
- package/src/lib/elements/theme.css +0 -6
- package/src/lib/host.svelte.ts +336 -0
- package/src/lib/index.ts +242 -196
- package/src/lib/layout.svelte.ts +52 -0
- package/src/lib/messages.ts +157 -77
- package/src/lib/peach-sdk.ts +86 -40
- package/src/lib/portal.ts +23 -0
- package/src/lib/CartExpiryGuard.test.ts +0 -331
- package/src/lib/CheckoutModal.confirm-outcome.test.ts +0 -91
- package/src/lib/CheckoutModal.payment-timeout.test.ts +0 -140
- package/src/lib/test/fixtures.ts +0 -107
- package/src/lib/test/messages-mock.ts +0 -34
|
@@ -1,196 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|