@code-collective/booking-widget 1.0.14 → 1.0.16
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 +75 -48
- package/README.md +506 -500
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +29 -52
- package/dist/booking-widget.min.js +6 -3
- package/dist/booking-widget.umd.cjs +2 -2
- package/package.json +1 -1
- package/src/lib/CheckoutModal.svelte +181 -1
- package/src/lib/api.ts +1 -28
- package/src/lib/checkout-payment-flow.svelte.ts +59 -103
- package/src/lib/generated-types.ts +0 -82
- package/src/lib/CheckoutModal.css +0 -179
package/README.md
CHANGED
|
@@ -1,500 +1,506 @@
|
|
|
1
|
-
# Booking Widget
|
|
2
|
-
|
|
3
|
-
A Svelte 5 widget library for embedding ticket sales on tourism websites. Three custom HTML elements handle product selection, cart management, and payment via Peach Payments.
|
|
4
|
-
|
|
5
|
-
## Option 1: HTML Custom Elements (CDN)
|
|
6
|
-
|
|
7
|
-
Drop in a script tag and use the custom elements directly. No framework or npm install required.
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<head>
|
|
11
|
-
<link rel="stylesheet"
|
|
12
|
-
href="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.
|
|
13
|
-
integrity="sha384-LDthHLROtZeufKbWmAhXBsAFLqqhkZjWFd8gWU+Ez11qyg5ebj9vpFnvJuZcM3ft"
|
|
14
|
-
crossorigin="anonymous" />
|
|
15
|
-
</head>
|
|
16
|
-
<body>
|
|
17
|
-
<bw-configurator product-id="your-product-id" checkout-key="your-checkout-key"></bw-configurator>
|
|
18
|
-
<bw-cart display="button"></bw-cart>
|
|
19
|
-
<bw-cart display="bar"></bw-cart>
|
|
20
|
-
<bw-checkout></bw-checkout>
|
|
21
|
-
|
|
22
|
-
<script
|
|
23
|
-
src="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.
|
|
24
|
-
integrity="sha384-
|
|
25
|
-
crossorigin="anonymous"></script>
|
|
26
|
-
</body>
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
> **Note:** Pin to a specific version (e.g. `@1.0.0`) when using SRI. The hashes change with every release. Run `npm run build:elements` to see the current hashes.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
| `bw:
|
|
106
|
-
| `bw:
|
|
107
|
-
| `bw:
|
|
108
|
-
| `bw:
|
|
109
|
-
| `bw:close` | -- | Checkout
|
|
110
|
-
| `bw:
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
|
179
|
-
|
|
180
|
-
|
|
|
181
|
-
| `
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
|
186
|
-
|
|
187
|
-
| `
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
|
192
|
-
|
|
193
|
-
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
|
198
|
-
|
|
199
|
-
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
|
301
|
-
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
|
|
305
|
-
`
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
[
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
|
491
|
-
|
|
492
|
-
| `--bw-color-
|
|
493
|
-
| `--bw-color-
|
|
494
|
-
| `--bw-color-
|
|
495
|
-
| `--bw-color-
|
|
496
|
-
| `--bw-
|
|
497
|
-
| `--bw-
|
|
498
|
-
| `--bw-
|
|
499
|
-
| `--bw-
|
|
500
|
-
| `--bw-
|
|
1
|
+
# Booking Widget
|
|
2
|
+
|
|
3
|
+
A Svelte 5 widget library for embedding ticket sales on tourism websites. Three custom HTML elements handle product selection, cart management, and payment via Peach Payments.
|
|
4
|
+
|
|
5
|
+
## Option 1: HTML Custom Elements (CDN)
|
|
6
|
+
|
|
7
|
+
Drop in a script tag and use the custom elements directly. No framework or npm install required.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<head>
|
|
11
|
+
<link rel="stylesheet"
|
|
12
|
+
href="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.15/dist/booking-widget.min.css"
|
|
13
|
+
integrity="sha384-LDthHLROtZeufKbWmAhXBsAFLqqhkZjWFd8gWU+Ez11qyg5ebj9vpFnvJuZcM3ft"
|
|
14
|
+
crossorigin="anonymous" />
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<bw-configurator product-id="your-product-id" checkout-key="your-checkout-key"></bw-configurator>
|
|
18
|
+
<bw-cart display="button"></bw-cart>
|
|
19
|
+
<bw-cart display="bar"></bw-cart>
|
|
20
|
+
<bw-checkout></bw-checkout>
|
|
21
|
+
|
|
22
|
+
<script
|
|
23
|
+
src="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.15/dist/booking-widget.min.js"
|
|
24
|
+
integrity="sha384-vbNVEDGKWm6wwoq+w8uD3mH8eye+XceceTkCL9vW6qd4YTK6WNqKK0Y0e7YiC541"
|
|
25
|
+
crossorigin="anonymous"></script>
|
|
26
|
+
</body>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Note:** Pin to a specific version (e.g. `@1.0.0`) when using SRI. The hashes change with every release. Run `npm run build:elements` to see the current hashes.
|
|
30
|
+
|
|
31
|
+
> **1.0.15** fixes a CSS regression from 1.0.13/1.0.14 where the checkout modal's own styles (cart card, pay bar, buttons) were silently missing from the built widget - anyone pinned to an affected version should upgrade.
|
|
32
|
+
|
|
33
|
+
The script auto-wires the elements together:
|
|
34
|
+
- Adding an item shows the cart and opens the checkout overlay
|
|
35
|
+
- Clicking checkout in the cart opens the overlay
|
|
36
|
+
- Closing or completing checkout closes the overlay
|
|
37
|
+
|
|
38
|
+
### Element attributes
|
|
39
|
+
|
|
40
|
+
#### `<bw-configurator>`
|
|
41
|
+
|
|
42
|
+
| Attribute | Required | Description |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `product-id` | Yes | OCTO product ID |
|
|
45
|
+
| `checkout-key` | Yes | Public key identifying the supplier |
|
|
46
|
+
| `wizard-pages` | No | JSON string for wizard step ordering |
|
|
47
|
+
| `auto-select-single-time-slot` | No | Boolean. Skips the time picker if only one slot |
|
|
48
|
+
| `cancelable` | No | Boolean. Shows a cancel button |
|
|
49
|
+
| `no-auto-checkout` | No | Boolean. Prevents auto-opening checkout on add |
|
|
50
|
+
| `on-cart-change` | No | Name of a global JS function to call on add |
|
|
51
|
+
|
|
52
|
+
#### `<bw-cart>`
|
|
53
|
+
|
|
54
|
+
| Attribute | Required | Description |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `checkout-key` | Yes | Public key identifying the supplier |
|
|
57
|
+
| `display` | No | `bar` (default) or `button` |
|
|
58
|
+
|
|
59
|
+
#### `<bw-checkout>`
|
|
60
|
+
|
|
61
|
+
| Attribute | Required | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `checkout-key` | Yes | Public key identifying the supplier |
|
|
64
|
+
| `wizard-pages` | No | JSON string for wizard step ordering |
|
|
65
|
+
| `edit-pages` | No | JSON string for how fields are grouped in the cart edit view |
|
|
66
|
+
| `auto-select-single-time-slot` | No | Boolean. Skips the time picker if only one slot |
|
|
67
|
+
|
|
68
|
+
### Global options
|
|
69
|
+
|
|
70
|
+
Set `window.bwOptions` before the widget script loads:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<script>
|
|
74
|
+
window.bwOptions = {
|
|
75
|
+
shouldBottomCloseOnModal: true,
|
|
76
|
+
autoSelectSingleTimeSlot: false,
|
|
77
|
+
wizardPages: '',
|
|
78
|
+
editPages: '',
|
|
79
|
+
};
|
|
80
|
+
</script>
|
|
81
|
+
<script src="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.15/dist/booking-widget.min.js"
|
|
82
|
+
integrity="sha384-vbNVEDGKWm6wwoq+w8uD3mH8eye+XceceTkCL9vW6qd4YTK6WNqKK0Y0e7YiC541"
|
|
83
|
+
crossorigin="anonymous"></script>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Option | Default | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `shouldBottomCloseOnModal` | `true` | Hide bar carts when checkout modal opens |
|
|
89
|
+
| `autoSelectSingleTimeSlot` | `false` | Auto-select (and hide) the time picker when only one slot |
|
|
90
|
+
| `wizardPages` | `''` | JSON wizard step ordering for all elements |
|
|
91
|
+
| `editPages` | `''` | JSON field grouping for the checkout edit view (`bw-checkout` only) |
|
|
92
|
+
|
|
93
|
+
### Window events
|
|
94
|
+
|
|
95
|
+
All `bw:*` events are re-dispatched on `window`:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
window.addEventListener('bw:order-confirmed', (e) => {
|
|
99
|
+
console.log(e.detail); // { cartToken, value, currency }
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Event | Detail | When |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `bw:cart-change` | `{ itemCount, cartItemId, totalFormatted }` | Item added to cart |
|
|
106
|
+
| `bw:cart-updated` | `{ cart }` (full `CheckoutCartDetailDto`, or `null` if empty) | Cart changed - add, edit, remove, extend, or expiry. Use this to build a custom cart summary (item count, remaining time - see Cart expiry below, item details via `cart.items`) instead of `bw-cart`. Fires with `cart: null` when the cart expires, same as a manual clear |
|
|
107
|
+
| `bw:order-confirmed` | `{ cartToken, value, currency }` | Payment completed and confirmed. Fires as soon as confirmation succeeds, while the success screen is still showing - it does not close the modal (see Payment outcomes below) |
|
|
108
|
+
| `bw:modal-open` | -- | Checkout modal opens |
|
|
109
|
+
| `bw:modal-close` | -- | Checkout modal closes |
|
|
110
|
+
| `bw:checkout` | -- | Cart checkout button clicked |
|
|
111
|
+
| `bw:close` | -- | Checkout dismissed |
|
|
112
|
+
| `bw:cancel` | -- | Configurator cancelled |
|
|
113
|
+
|
|
114
|
+
### Payment outcomes
|
|
115
|
+
|
|
116
|
+
Peach's own charge and this gateway's confirmation of it are two separate steps - the charge succeeds immediately, but confirmation depends on a webhook that can lag by a few seconds. Confirmation is entirely server-driven: the widget never confirms a cart itself, it only polls the checkout API's status endpoint and renders whatever it reports. A shopper who closes the tab mid-payment is still resolved correctly - nothing depends on their browser staying open.
|
|
117
|
+
|
|
118
|
+
**A declined card never reaches this widget.** Peach re-prompts inside its own embedded form, as many times as it allows, and the shopper only comes back here once they have paid or given up. So there is no "your card was declined" screen and no retry counter: the retry happens on Peach's form, on the same checkout. The widget hears from Peach only when the checkout itself ends - completed, cancelled, expired, or broken.
|
|
119
|
+
|
|
120
|
+
The checkout modal reflects the outcome as distinct result screens, and stays open until the shopper dismisses it themselves (Done/Close) rather than closing automatically:
|
|
121
|
+
|
|
122
|
+
| Outcome | Shown when | Shopper can retry payment? |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| Payment Successful | Charge succeeded and confirmed | -- |
|
|
125
|
+
| Still Checking Your Payment (pending) | No final answer within the poll's window - typically a charge whose webhook is slow | No - a "Check Again" button re-checks on demand instead |
|
|
126
|
+
| Payment Failed (not charged) | Peach reports the checkout expired, errored, or the shopper backed out; or a server-side sweep released a cart nobody came back to | Yes - nothing was charged, and Try Again mints a brand-new Peach checkout |
|
|
127
|
+
| Booking Incomplete (partial) | Charge succeeded but some cart items didn't confirm | No - the charge already happened; the shopper is told to contact support instead |
|
|
128
|
+
|
|
129
|
+
`bw:order-confirmed` only fires for the first outcome. A shopper who dismisses the "pending" screen before it resolves gets no order-confirmed event even though the payment may still confirm moments later via the webhook - build any "wait for confirmation" UI around the event, not around the modal closing.
|
|
130
|
+
|
|
131
|
+
### Cart expiry
|
|
132
|
+
|
|
133
|
+
A cart is created with a 15-minute idle window and a hard ceiling of 35 minutes from creation. Every action on the cart - adding an item, editing one, starting payment, reopening an abandoned payment attempt, and answering the "Are you still there?" prompt - resets the idle window to a full 15 minutes from that moment, but never past the ceiling. `cart.idleExpiresAt` is therefore always the cart's real deadline, and `cart.absoluteExpiresAt` is the ceiling it can never move beyond. If a supplier hold behind one of the items cannot be extended, the cart's deadline is left where it was, so the cart never outlives the holds it fronts.
|
|
134
|
+
|
|
135
|
+
At two minutes remaining, an "Are you still there?" prompt appears. Unlike the rest of the widget's UI it is page-wide: it renders above whatever the shopper is looking at, whether or not a `<bw-checkout>` is even open, since a merchant might embed only `<bw-configurator>`/`<bw-cart>` with no checkout element on the page at all. Answering "Yes, I'm still here" resets the idle window as above and fires `bw:cart-updated` with the extended cart. Once the idle deadline has already reached the ceiling - or an answer came back with the deadline unmoved because a supplier hold could not be extended - the prompt only warns and offers OK: there is no more time to be had. While a payment is in progress the cart is exempt from expiry and cannot be extended (Peach's own session cannot be either), so the prompt does not appear over the card form; if the payment is cancelled or fails and the cart reopens, the countdown resumes from the server's deadline. A cart left sitting on Peach's form past its hard ceiling is released by a server-side sweep, which asks Peach first and never releases one Peach reports as paid; the widget shows that as Payment Failed (not charged). A page reload during payment is remembered: reopening checkout returns the shopper to the same card form, or offers to start over if Peach no longer accepts it.
|
|
136
|
+
|
|
137
|
+
If the deadline passes unanswered, the widget checks with the server and, once the server confirms it has let the cart go, clears it: a "Your cart has expired" message replaces the prompt, `bw:cart-updated` fires with `cart: null`, and there is no way back into the expired cart - only a fresh one. The browser's clock only decides when to ask; a clock running ahead of the server's never throws away a live cart. If you compute your own "time remaining", read it from `cart.idleExpiresAt`.
|
|
138
|
+
|
|
139
|
+
The prompt, the extension it offers, and the automatic clear-on-expiry come with `createBookingHost()`, which mounts `CartExpiryGuard` on `<body>` once per page - so every option below has them, not just the custom-elements script. Pass `mountExpiryGuard: false` to opt out, or mount `<CartExpiryGuard />` yourself. Only a consumer building `ApiClient` / `SessionManager` / `CartManager` by hand goes without: then the cart still expires server-side on the same schedule and `onCartUpdated` still reports `cart: null` once the widget next hears the server refuse it, but nothing warns the shopper beforehand.
|
|
140
|
+
|
|
141
|
+
## Option 2: JavaScript mount functions
|
|
142
|
+
|
|
143
|
+
For programmatic control in an Astro, Vite, or bundled project, install the package and import the ES module:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install @code-collective/booking-widget
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
import { createBookingHost, mountConfigurator, mountCheckout } from '@code-collective/booking-widget';
|
|
151
|
+
import '@code-collective/booking-widget/style.css';
|
|
152
|
+
|
|
153
|
+
// One host for the page: one session, one cart, one expiry guard. Pass it to every mount function that
|
|
154
|
+
// should share them - without it, each mount builds its own and you get two of everything.
|
|
155
|
+
const host = createBookingHost({
|
|
156
|
+
checkoutKey: 'your-checkout-key',
|
|
157
|
+
apiBaseUrl: 'https://checkout.yourdomain.com',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const configurator = await mountConfigurator(document.getElementById('configurator'), {
|
|
161
|
+
host,
|
|
162
|
+
productId: 'your-product-id',
|
|
163
|
+
onCartChange({ itemCount, totalFormatted }) {
|
|
164
|
+
console.log(`${itemCount} items, ${totalFormatted}`);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Adding an item opens this automatically - see Host wiring below.
|
|
169
|
+
const checkout = await mountCheckout(document.getElementById('checkout'), { host, mode: 'modal' });
|
|
170
|
+
|
|
171
|
+
// Later: configurator.destroy(); checkout.destroy(); host.destroy();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Mount functions
|
|
175
|
+
|
|
176
|
+
**`mountConfigurator(target, config)`** — Product selection wizard
|
|
177
|
+
|
|
178
|
+
| Config | Required | Description |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `productId` | Yes | OCTO product ID |
|
|
181
|
+
| `host` | No | A `BookingHost` to share (see [Host wiring](#host-wiring)). Without one, this mount builds its own |
|
|
182
|
+
| `checkoutKey` | No | Supplier checkout key |
|
|
183
|
+
| `apiBaseUrl` | No | Checkout API URL |
|
|
184
|
+
| `wizardPages` | No | Wizard step ordering |
|
|
185
|
+
| `autoSelectSingleTimeSlot` | No | Skip time picker if single slot |
|
|
186
|
+
| Cart callbacks | No | `onCartChange`, `onCartUpdated`, `onCartExpired`, `onOrderConfirmed` - see [Host wiring](#host-wiring) |
|
|
187
|
+
| `onCancel` | No | Callback when cancelled |
|
|
188
|
+
|
|
189
|
+
**`mountCheckout(target, config)`** — Cart review, contact form, payment
|
|
190
|
+
|
|
191
|
+
| Config | Required | Description |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `host` | No | A `BookingHost` to share (see [Host wiring](#host-wiring)). Without one, this mount builds its own |
|
|
194
|
+
| `checkoutKey` | No | Supplier checkout key |
|
|
195
|
+
| `apiBaseUrl` | No | Checkout API URL |
|
|
196
|
+
| `mode` | No | `inline` (default) or `modal` - a portalled overlay with a scrim |
|
|
197
|
+
| `onClose` | No | Callback when closed |
|
|
198
|
+
| `onOrderConfirmed` | No | Callback: `({ cartToken, value, currency })` |
|
|
199
|
+
| Cart callbacks | No | `onCartChange`, `onCartUpdated`, `onCartExpired`, `onOrderConfirmed` - see [Host wiring](#host-wiring) |
|
|
200
|
+
|
|
201
|
+
**`mountCartOverview(target, config)`** — Cart summary
|
|
202
|
+
|
|
203
|
+
| Config | Required | Description |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `host` | No | A `BookingHost` to share (see [Host wiring](#host-wiring)). Without one, this mount builds its own |
|
|
206
|
+
| `checkoutKey` | No | Supplier checkout key |
|
|
207
|
+
| `apiBaseUrl` | No | Checkout API URL |
|
|
208
|
+
| `display` | No | `bar` or `button` |
|
|
209
|
+
| `onCheckout` | No | Callback when checkout clicked |
|
|
210
|
+
| Cart callbacks | No | `onCartChange`, `onCartUpdated`, `onCartExpired`, `onOrderConfirmed` - see [Host wiring](#host-wiring) |
|
|
211
|
+
|
|
212
|
+
All mount functions return `Promise<{ destroy(): void }>`. Every config also accepts the
|
|
213
|
+
[host options](#host-wiring) (`autoOpenCheckout`, `wizardPages`, `autoSelectSingleTimeSlot`, `onCartUpdated`,
|
|
214
|
+
...), which are used to build the host when you do not pass one.
|
|
215
|
+
|
|
216
|
+
The page-wide "Are you still there?" prompt and extend flow come with the host, so this option has them.
|
|
217
|
+
|
|
218
|
+
## Option 3: Svelte components
|
|
219
|
+
|
|
220
|
+
Install the package and import Svelte components directly for full reactivity:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm install @code-collective/booking-widget
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```svelte
|
|
227
|
+
<script lang="ts">
|
|
228
|
+
import { createBookingHost, BookingProvider, TicketConfigurator, Checkout, CartOverviewButton } from '@code-collective/booking-widget';
|
|
229
|
+
import '@code-collective/booking-widget/style.css';
|
|
230
|
+
|
|
231
|
+
let host = $state<BookingHost | null>(null);
|
|
232
|
+
let ready = $state(false);
|
|
233
|
+
|
|
234
|
+
// In onMount, not the script body: anything server-rendered (an Astro island, SvelteKit SSR) runs the
|
|
235
|
+
// body once with no DOM, and a host built there opens a session the hydrated page throws away.
|
|
236
|
+
onMount(() => {
|
|
237
|
+
const created = createBookingHost({
|
|
238
|
+
apiBaseUrl: 'https://checkout.yourdomain.com',
|
|
239
|
+
checkoutKey: 'your-checkout-key',
|
|
240
|
+
});
|
|
241
|
+
host = created;
|
|
242
|
+
// ready rejects if the checkout service turns the session down - usually the key not being allowlisted
|
|
243
|
+
// for this origin. Uncaught, the page sits on its loading state forever.
|
|
244
|
+
created.ready.then(() => { ready = true; }).catch(() => { /* render your own fallback */ });
|
|
245
|
+
return () => created.destroy();
|
|
246
|
+
});
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
{#if ready && host}
|
|
250
|
+
<BookingProvider host={host}>
|
|
251
|
+
<TicketConfigurator productId="your-product-id" />
|
|
252
|
+
<CartOverviewButton />
|
|
253
|
+
<Checkout mode="modal" />
|
|
254
|
+
</BookingProvider>
|
|
255
|
+
{/if}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Adding an item opens the checkout, the modal portals itself out of any sticky or transformed ancestor, and
|
|
259
|
+
the page-wide "Are you still there?" prompt is mounted for you - all from the host, with no wiring of your
|
|
260
|
+
own. See [Host wiring](#host-wiring).
|
|
261
|
+
|
|
262
|
+
`BookingProvider` puts the host in Svelte context, which is why the components inside it need no `api` or
|
|
263
|
+
`cartManager`, and why `Checkout` needs no `open` or `onClose`. Every one of those props still exists and
|
|
264
|
+
still wins when you pass it - the `bw-*` custom elements pass them explicitly, since context does not cross
|
|
265
|
+
a custom element's own root. Without a provider and without the prop, a component throws and names both
|
|
266
|
+
ways to fix it rather than failing somewhere deeper.
|
|
267
|
+
|
|
268
|
+
Building the services by hand (`new ApiClient(...)`, `new SessionManager(...)`, `new CartManager(...)`) still
|
|
269
|
+
works and is what `createBookingHost` does internally, but none of the behaviour above comes with it. If you
|
|
270
|
+
go that route, mount `CartExpiryGuard` yourself:
|
|
271
|
+
|
|
272
|
+
```svelte
|
|
273
|
+
<CartExpiryGuard {api} {cartManager} />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Host wiring
|
|
277
|
+
|
|
278
|
+
`createBookingHost(config)` is the one place the widget's host behaviour lives. The custom-elements build is
|
|
279
|
+
a thin DOM adapter over it, so both builds auto-wire through exactly the same code.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const host = createBookingHost({
|
|
283
|
+
apiBaseUrl, checkoutKey,
|
|
284
|
+
autoOpenCheckout: true, // default - opt out, never opt in
|
|
285
|
+
shouldBottomCloseOnModal: true, // hide bar-display carts while checkout is open
|
|
286
|
+
onCartChange({ itemCount, cartItemId, totalFormatted }) {}, // an add, with its own detail
|
|
287
|
+
onCartUpdated(cart) {}, // any change, with the whole cart
|
|
288
|
+
onCartExpired() {}, // the cart's window ran out
|
|
289
|
+
onCheckoutOpenChange(open) {},
|
|
290
|
+
onOrderConfirmed({ cartToken, value, currency }) {},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
host.api / host.cartManager / host.sessionManager // one set, shared by every component
|
|
294
|
+
host.ready // resolves once the session exists; rejects if it is refused
|
|
295
|
+
host.isCheckoutOpen // reactive
|
|
296
|
+
host.openCheckout() / host.closeCheckout()
|
|
297
|
+
host.destroy()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
| Option | Type | Default | Description |
|
|
301
|
+
|---|---|---|---|
|
|
302
|
+
| `autoOpenCheckout` | `boolean \| (() => boolean)` | `true` | Whether adding an item opens checkout. A function is re-asked on every add, so a layout that already shows the configurator inline can answer `false` for that case only |
|
|
303
|
+
| `shouldBottomCloseOnModal` | `boolean` | `true` | Hide bar-display cart overviews while checkout is open |
|
|
304
|
+
| `autoSelectSingleTimeSlot` | `boolean` | `false` | Skip the time picker when only one slot is available |
|
|
305
|
+
| `wizardPages` / `editPages` | `WizardPages` | - | Wizard step ordering, handed to every component the host serves |
|
|
306
|
+
| `mountExpiryGuard` | `boolean` | `true` | Mount `CartExpiryGuard` on `<body>` |
|
|
307
|
+
| `peachEnv` | `'prod' \| 'qa'` | `'prod'` | Which Peach Payments SDK the card form loads. **Set it whenever `apiBaseUrl` is not production** - the checkout API creates the Peach checkout, so a sandbox API with the production SDK gives a card form that refuses to render |
|
|
308
|
+
| `peachSdkUrl` | `string` | - | An explicit Peach SDK URL, for an environment `peachEnv` does not name |
|
|
309
|
+
| `api` | `BookingApi` | - | An API client to use instead of building one from `apiBaseUrl` - a test double, or a fake for a demo page |
|
|
310
|
+
|
|
311
|
+
`setBookingDefaults({ ... })` applies the same options site-wide to every host created afterwards - the ES
|
|
312
|
+
equivalent of `window.bwOptions`. Anything passed to `createBookingHost` wins over a default.
|
|
313
|
+
|
|
314
|
+
### Callbacks
|
|
315
|
+
|
|
316
|
+
| Callback | Fires |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `onCartChange` | On an add, with the detail only the add knows: `{ itemCount, cartItemId, totalFormatted }`. Fires whether or not checkout opened |
|
|
319
|
+
| `onCartUpdated` | After any add, edit or remove, carrying the whole cart the widget already fetched. What a custom cart summary reads |
|
|
320
|
+
| `onCartExpired` | The cart's window ran out - distinct from the `null` `onCartUpdated` that a confirmed order also produces |
|
|
321
|
+
| `onCheckoutOpenChange` | Checkout opened or closed, once per change. `false` after an order means the shopper dismissed the result - this is the one to navigate on |
|
|
322
|
+
| `onOrderConfirmed` | Payment succeeded *and* was confirmed - not while a webhook is still pending. `{ cartToken, value, currency }`. Fires while the shopper is still looking at the result screen, so do not close or navigate on it |
|
|
323
|
+
|
|
324
|
+
Each is **one slot, fixed when the host is built**: set it twice and the second wins, and there is no
|
|
325
|
+
detaching it short of `host.destroy()`. Where more than one part of the page needs the same event, or a
|
|
326
|
+
subscription should come and go with a component, subscribe to the message bus instead - see below. The two
|
|
327
|
+
ride the same bus and fire at the same moment.
|
|
328
|
+
|
|
329
|
+
The mount functions take these on their own config too. When you hand them a `host`, the callbacks on that
|
|
330
|
+
config are wired to the same events rather than dropped - but each event still reaches your handler once.
|
|
331
|
+
|
|
332
|
+
### Messages
|
|
333
|
+
|
|
334
|
+
`onWidgetMessage(handler)` subscribes to the widget's own message bus and returns an unsubscribe function;
|
|
335
|
+
`postMessage(message)` sends one. Both are exported, and every message is a member of the `WidgetMessage`
|
|
336
|
+
union (`cart:change`, `cart:updated`, `cart:expired`, `modal:open`, `modal:close`, `order:complete`,
|
|
337
|
+
`payment:started`, `payment:ended`, `payment:timed-out`), so a handler narrows on `message.type`.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { onWidgetMessage } from '@code-collective/booking-widget';
|
|
341
|
+
|
|
342
|
+
const stop = onWidgetMessage((m) => {
|
|
343
|
+
if (m.type === 'cart:change') track('add_to_cart', m.totalFormatted);
|
|
344
|
+
});
|
|
345
|
+
// later
|
|
346
|
+
stop();
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Prefer the host callbacks where they cover what you need - they are the same events, already parsed. Reach
|
|
350
|
+
for `onWidgetMessage` when you need several subscribers, or one that comes and goes.
|
|
351
|
+
|
|
352
|
+
## Astro integration
|
|
353
|
+
|
|
354
|
+
### Script tag approach
|
|
355
|
+
|
|
356
|
+
```astro
|
|
357
|
+
---
|
|
358
|
+
const product = await getProduct(Astro.params.slug);
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
<bw-configurator product-id={product.id} checkout-key={product.checkoutKey}></bw-configurator>
|
|
362
|
+
<bw-cart display="bar" checkout-key={product.checkoutKey}></bw-cart>
|
|
363
|
+
<bw-checkout checkout-key={product.checkoutKey}></bw-checkout>
|
|
364
|
+
|
|
365
|
+
<link rel="stylesheet"
|
|
366
|
+
href="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.15/dist/booking-widget.min.css"
|
|
367
|
+
integrity="sha384-LDthHLROtZeufKbWmAhXBsAFLqqhkZjWFd8gWU+Ez11qyg5ebj9vpFnvJuZcM3ft"
|
|
368
|
+
crossorigin="anonymous" />
|
|
369
|
+
<script is:inline
|
|
370
|
+
src="https://cdn.jsdelivr.net/npm/@code-collective/booking-widget@1.0.15/dist/booking-widget.min.js"
|
|
371
|
+
integrity="sha384-vbNVEDGKWm6wwoq+w8uD3mH8eye+XceceTkCL9vW6qd4YTK6WNqKK0Y0e7YiC541"
|
|
372
|
+
crossorigin="anonymous"></script>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Svelte island approach
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
npx astro add svelte
|
|
379
|
+
npm install @code-collective/booking-widget
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```astro
|
|
383
|
+
---
|
|
384
|
+
import BookingWidget from '../components/BookingWidget.svelte';
|
|
385
|
+
const product = await getProduct(Astro.params.slug);
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
<BookingWidget
|
|
389
|
+
client:load
|
|
390
|
+
productId={product.id}
|
|
391
|
+
checkoutKey={product.checkoutKey}
|
|
392
|
+
apiBaseUrl="https://checkout.yourdomain.com"
|
|
393
|
+
/>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Wizard pages
|
|
397
|
+
|
|
398
|
+
Each wizard page is an object with a `title` (shown as the page heading and accordion label) and a `widgets` array that controls which sections appear on that page.
|
|
399
|
+
|
|
400
|
+
**Option-first (default):**
|
|
401
|
+
```json
|
|
402
|
+
[
|
|
403
|
+
{ "title": "Option", "widgets": ["option", "age-category"] },
|
|
404
|
+
{ "title": "Schedule", "widgets": ["date", "time"] },
|
|
405
|
+
{ "title": "Pickup", "widgets": ["pickup"] }
|
|
406
|
+
]
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Date-first:**
|
|
410
|
+
```json
|
|
411
|
+
[
|
|
412
|
+
{ "title": "Age & capacity", "widgets": ["age-category"] },
|
|
413
|
+
{ "title": "Schedule", "widgets": ["date", "time"] },
|
|
414
|
+
{ "title": "Option", "widgets": ["option"] },
|
|
415
|
+
{ "title": "Pickup", "widgets": ["pickup"] }
|
|
416
|
+
]
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Available widget types: `option`, `age-category`, `date`, `time`, `pickup`.
|
|
420
|
+
|
|
421
|
+
### Setting wizard pages
|
|
422
|
+
|
|
423
|
+
**Using a preset** (simplest):
|
|
424
|
+
```html
|
|
425
|
+
<bw-configurator product-id="..." checkout-key="..." wizard-pages="option-first"></bw-configurator>
|
|
426
|
+
|
|
427
|
+
<bw-configurator product-id="..." checkout-key="..." wizard-pages="date-first"></bw-configurator>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Using a custom JSON array** (full control):
|
|
431
|
+
```html
|
|
432
|
+
<bw-configurator product-id="..." checkout-key="..."
|
|
433
|
+
wizard-pages='[
|
|
434
|
+
{"title":"Age & capacity","widgets":["age-category"]},
|
|
435
|
+
{"title":"Schedule","widgets":["date","time"]},
|
|
436
|
+
{"title":"Option","widgets":["option"]},
|
|
437
|
+
{"title":"Pickup","widgets":["pickup"]}
|
|
438
|
+
]'>
|
|
439
|
+
</bw-configurator>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
If `wizard-pages` is omitted, the default is `option-first`.
|
|
443
|
+
|
|
444
|
+
In the configurator, pages are shown as a step-by-step wizard with dot indicators. In the checkout edit view, pages are shown as collapsible accordion sections with a summary of the selected values when collapsed.
|
|
445
|
+
|
|
446
|
+
### Edit pages (checkout accordion grouping)
|
|
447
|
+
|
|
448
|
+
`edit-pages` controls the same thing as `wizard-pages` — which widgets appear together, and in what order — but for the collapsible accordion shown when editing an item already in the cart (`<bw-checkout>` only; it has no effect on `<bw-configurator>`'s step wizard). It accepts the same two shapes as `wizard-pages`: a preset name (`"option-first"` / `"date-first"`) or a custom JSON array of `{ "title", "widgets" }` objects.
|
|
449
|
+
|
|
450
|
+
```html
|
|
451
|
+
<bw-checkout checkout-key="..." wizard-pages="date-first" edit-pages="date-first"></bw-checkout>
|
|
452
|
+
|
|
453
|
+
<bw-checkout checkout-key="..."
|
|
454
|
+
edit-pages='[
|
|
455
|
+
{"title":"Age, Date & Time","widgets":["age-category","date","time"]},
|
|
456
|
+
{"title":"Option","widgets":["option"]},
|
|
457
|
+
{"title":"Pickup","widgets":["pickup"]}
|
|
458
|
+
]'>
|
|
459
|
+
</bw-checkout>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
If `edit-pages` is omitted, it's derived from `wizard-pages`: age/date/time are grouped into one accordion section, positioned first for `date-first` and second (after Option) for `option-first`. If `wizard-pages` is itself a custom JSON array rather than a named preset, there's no sensible grouping to infer, so `edit-pages` just mirrors it directly — set `edit-pages` explicitly in that case if you want different grouping in the edit view.
|
|
463
|
+
|
|
464
|
+
While an item is being edited, changing a field can invalidate an earlier selection (e.g. increasing ticket quantity beyond the previously-selected time slot's vacancies resets the date/time). An invalidated accordion section is shown with a red border and a "Changes made require new configuration" summary — even while collapsed — until it's reselected. Fields that don't need to change are preserved automatically (e.g. an already-chosen pickup point stays selected across a date change, since pickup is a property of the option, not the date/time).
|
|
465
|
+
|
|
466
|
+
### Hiding the time selector
|
|
467
|
+
|
|
468
|
+
Some products only ever offer a single time slot per day (or an all-day slot with no specific time). For these, showing a time picker step with only one option to click is pointless friction. Set `auto-select-single-time-slot` to skip it:
|
|
469
|
+
|
|
470
|
+
```html
|
|
471
|
+
<bw-configurator product-id="..." checkout-key="..." auto-select-single-time-slot></bw-configurator>
|
|
472
|
+
<bw-checkout checkout-key="..." auto-select-single-time-slot></bw-checkout>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
When enabled, and the selected date has exactly one available time slot, that slot is selected automatically and the time-picker widget is hidden entirely — the flow moves straight from date to the next widget (option or pickup, depending on `wizard-pages`). A date with a genuine all-day slot (`allDay: true` from the availability API) always behaves this way, regardless of this flag. Set it on both `<bw-configurator>` and `<bw-checkout>` (or via `window.bwOptions.autoSelectSingleTimeSlot`, which wires it onto every element automatically) so the behavior is consistent between adding an item and editing one already in the cart.
|
|
476
|
+
|
|
477
|
+
## Theming
|
|
478
|
+
|
|
479
|
+
The widget reads CSS custom properties. It automatically picks up site variables (`--header-background`, `--default-font-family`, `--radius`) or can be themed directly:
|
|
480
|
+
|
|
481
|
+
```css
|
|
482
|
+
:root {
|
|
483
|
+
--bw-color-primary: #0066CC;
|
|
484
|
+
--bw-color-primary-dark: #004C99;
|
|
485
|
+
--bw-font-family: 'Inter', sans-serif;
|
|
486
|
+
--bw-radius-md: 4px;
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
| Variable | Fallback | Default |
|
|
491
|
+
|---|---|---|
|
|
492
|
+
| `--bw-color-primary` | `--header-background` | `#E30613` |
|
|
493
|
+
| `--bw-color-primary-dark` | `--footer-background` | `#C00510` |
|
|
494
|
+
| `--bw-color-primary-light` | -- | `rgba(227,6,19,0.08)` |
|
|
495
|
+
| `--bw-color-text` | -- | `#212121` |
|
|
496
|
+
| `--bw-color-text-secondary` | -- | `#757575` |
|
|
497
|
+
| `--bw-color-border` | -- | `#E0E0E0` |
|
|
498
|
+
| `--bw-color-bg` | -- | `#FFFFFF` |
|
|
499
|
+
| `--bw-color-surface` | -- | `#F5F5F5` |
|
|
500
|
+
| `--bw-color-success` | -- | `#4CAF50` |
|
|
501
|
+
| `--bw-color-error` | `--header-background` | `#E30613` |
|
|
502
|
+
| `--bw-font-family` | `--default-font-family` | `Roboto, system` |
|
|
503
|
+
| `--bw-radius-sm` | -- | `4px` |
|
|
504
|
+
| `--bw-radius-md` | `--radius` | `8px` |
|
|
505
|
+
| `--bw-radius-lg` | -- | `12px` |
|
|
506
|
+
| `--bw-transition` | -- | `0.15s ease` |
|