@code-collective/booking-widget 1.0.12 → 1.0.14

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 (38) hide show
  1. package/CHANGELOG.md +48 -46
  2. package/README.md +500 -500
  3. package/dist/booking-widget.css +1 -1
  4. package/dist/booking-widget.js +829 -707
  5. package/dist/booking-widget.min.js +24 -25
  6. package/dist/booking-widget.umd.cjs +5 -5
  7. package/package.json +58 -58
  8. package/src/lib/BookingProvider.svelte +21 -21
  9. package/src/lib/CartBar.svelte +26 -26
  10. package/src/lib/CartBarView.svelte +78 -78
  11. package/src/lib/CartExpiryGuard.svelte +416 -416
  12. package/src/lib/CartOverview.svelte +30 -30
  13. package/src/lib/CartOverviewButton.svelte +104 -104
  14. package/src/lib/Checkout.svelte +141 -141
  15. package/src/lib/CheckoutModal.css +179 -0
  16. package/src/lib/CheckoutModal.svelte +78 -573
  17. package/src/lib/CheckoutPanel.svelte +121 -121
  18. package/src/lib/PaymentPage.svelte +208 -191
  19. package/src/lib/ResultView.svelte +24 -4
  20. package/src/lib/TicketConfigurator.svelte +166 -166
  21. package/src/lib/api.ts +36 -5
  22. package/src/lib/booking-context.ts +33 -33
  23. package/src/lib/cart-overview.svelte.ts +97 -97
  24. package/src/lib/checkout-item-view.ts +63 -0
  25. package/src/lib/checkout-payment-flow.svelte.ts +523 -0
  26. package/src/lib/client-types.ts +22 -6
  27. package/src/lib/config.ts +162 -162
  28. package/src/lib/elements/bw-cart.svelte +49 -49
  29. package/src/lib/elements/bw-checkout.svelte +97 -97
  30. package/src/lib/elements/bw-configurator.svelte +72 -72
  31. package/src/lib/elements/register.ts +171 -171
  32. package/src/lib/elements/shared.ts +18 -18
  33. package/src/lib/generated-types.ts +94 -5
  34. package/src/lib/host.svelte.ts +336 -336
  35. package/src/lib/index.ts +242 -242
  36. package/src/lib/messages.ts +157 -157
  37. package/src/lib/peach-sdk.ts +86 -86
  38. package/src/lib/portal.ts +23 -23
@@ -1,121 +1,121 @@
1
- <script lang="ts">
2
- import type { BookingApi } from './api';
3
- import type { WizardPages } from './config';
4
- import { DEFAULT_WIZARD_PAGES } from './config';
5
- import type { CheckoutCartDetailDto } from './client-types';
6
- import { isInvalidCart } from './api';
7
- import { onWidgetMessage, postMessage } from './messages';
8
- import type { CartManager } from './cart-manager';
9
- import CheckoutModal from './CheckoutModal.svelte';
10
- import CartExpiredView from './CartExpiredView.svelte';
11
-
12
- interface Props {
13
- api: BookingApi;
14
- cartManager?: CartManager;
15
- wizardPages?: WizardPages;
16
- editPages?: WizardPages;
17
- autoSelectSingleTimeSlot?: boolean;
18
- }
19
- let { api, cartManager, wizardPages = DEFAULT_WIZARD_PAGES, editPages: editPagesProp,
20
- autoSelectSingleTimeSlot = false }: Props = $props();
21
- let editPages = $derived(editPagesProp ?? wizardPages);
22
-
23
- let cart = $state<CheckoutCartDetailDto | null>(null);
24
- let isLoading = $state(true);
25
- let expired = $state(false);
26
-
27
- async function load() {
28
- // A token this widget still holds for a cart the server has already let go (its window ran out while the
29
- // tab sat there) used to render as a bare "No items in cart", with the dead token left in storage for the
30
- // next add to trip over. Only a held token can mean that - with none, an empty cart is just empty.
31
- const hadCart = api.cartToken !== '';
32
- try {
33
- cart = await api.getCart();
34
- } catch (e) {
35
- cart = null;
36
- if (hadCart && isInvalidCart(e)) {
37
- onCartExpired();
38
- expired = true;
39
- }
40
- }
41
- isLoading = false;
42
- }
43
-
44
- load();
45
-
46
- // Drops the stored token so whatever the shopper does next starts a fresh cart, and clears bw-cart's
47
- // bar/button and any host-side summary built from bw:cart-updated - the cart they were describing no longer
48
- // exists. Mirrors what onOrderConfirmed below does once a cart has served its purpose the other way.
49
- function onCartExpired() {
50
- cartManager?.reset();
51
- postMessage({ type: 'cart:updated', cart: null });
52
- postMessage({ type: 'cart:expired' });
53
- }
54
-
55
- // In modal mode, modal:close is what actually dismisses this screen - bw-checkout.svelte remounts this
56
- // component fresh the next time it opens, so the click is fully handled there. Rendered permanently
57
- // in-page instead, nothing is listening for modal:close, so without the reload below the click would do
58
- // nothing a shopper can see: expired flips off, but cart is still the stale null from the load() that
59
- // found it gone, leaving the same "No items in cart" text up with no sign anything happened.
60
- function startAgain() {
61
- expired = false;
62
- postMessage({ type: 'modal:close' });
63
- void load();
64
- }
65
-
66
- // In modal mode this is redundant - bw-checkout.svelte only mounts this component fresh each time the
67
- // modal opens, so it already gets a current cart. Rendered permanently in-page instead (no is-modal), it
68
- // would otherwise only ever see the cart as it was on first mount. The event already carries the fresh
69
- // cart (see TicketConfigurator/CheckoutModal's own posting sites), so this applies it directly rather than
70
- // triggering a second, redundant getCart() call or flashing the spinner over an already-visible cart.
71
- $effect(() => onWidgetMessage((d) => {
72
- if (d.type === 'cart:updated' && 'cart' in d) cart = d.cart as CheckoutCartDetailDto | null;
73
- }));
74
- </script>
75
-
76
- <div class="bw-widget">
77
- {#if isLoading}
78
- <div class="loading-center" style="height:100vh">
79
- <div class="spinner"></div>
80
- </div>
81
- {:else if cart && cart.items.length > 0}
82
- <CheckoutModal
83
- {cart}
84
- {api}
85
- {wizardPages}
86
- {editPages}
87
- {autoSelectSingleTimeSlot}
88
- onClose={() => postMessage({ type: 'modal:close' })}
89
- onOrderConfirmed={() => {
90
- const total = cart!.items.reduce((s, i) => s + i.amount, 0);
91
- const currency = cart!.items[0]?.currencyCode ?? 'ZAR';
92
- postMessage({
93
- type: 'order:complete',
94
- cartToken: cart!.cartToken ?? '',
95
- value: total,
96
- currency,
97
- });
98
- // The cart is paid and confirmed, so its token has done its job. Nothing used to clear it, so it sat
99
- // in localStorage on the merchant's origin until absoluteExpiresAt - readable by every third-party
100
- // script they load, and picked up by the next person to use a shared or kiosk browser.
101
- cartManager?.reset();
102
- }}
103
- />
104
- {:else if expired}
105
- <div style="height:100vh">
106
- <CartExpiredView onStartAgain={startAgain} />
107
- </div>
108
- {:else}
109
- <div class="loading-center" style="height:100vh;color:var(--bw-color-text-secondary)">
110
- No items in cart.
111
- </div>
112
- {/if}
113
- </div>
114
-
115
- <style>
116
- /* display: contents - a plain box here would break CheckoutModal's own .modal{height:100%}, which needs
117
- to resolve against this component's real parent, not an unsized wrapper inserted in between. */
118
- .bw-widget {
119
- display: contents;
120
- }
121
- </style>
1
+ <script lang="ts">
2
+ import type { BookingApi } from './api';
3
+ import type { WizardPages } from './config';
4
+ import { DEFAULT_WIZARD_PAGES } from './config';
5
+ import type { CheckoutCartDetailDto } from './client-types';
6
+ import { isInvalidCart } from './api';
7
+ import { onWidgetMessage, postMessage } from './messages';
8
+ import type { CartManager } from './cart-manager';
9
+ import CheckoutModal from './CheckoutModal.svelte';
10
+ import CartExpiredView from './CartExpiredView.svelte';
11
+
12
+ interface Props {
13
+ api: BookingApi;
14
+ cartManager?: CartManager;
15
+ wizardPages?: WizardPages;
16
+ editPages?: WizardPages;
17
+ autoSelectSingleTimeSlot?: boolean;
18
+ }
19
+ let { api, cartManager, wizardPages = DEFAULT_WIZARD_PAGES, editPages: editPagesProp,
20
+ autoSelectSingleTimeSlot = false }: Props = $props();
21
+ let editPages = $derived(editPagesProp ?? wizardPages);
22
+
23
+ let cart = $state<CheckoutCartDetailDto | null>(null);
24
+ let isLoading = $state(true);
25
+ let expired = $state(false);
26
+
27
+ async function load() {
28
+ // A token this widget still holds for a cart the server has already let go (its window ran out while the
29
+ // tab sat there) used to render as a bare "No items in cart", with the dead token left in storage for the
30
+ // next add to trip over. Only a held token can mean that - with none, an empty cart is just empty.
31
+ const hadCart = api.cartToken !== '';
32
+ try {
33
+ cart = await api.getCart();
34
+ } catch (e) {
35
+ cart = null;
36
+ if (hadCart && isInvalidCart(e)) {
37
+ onCartExpired();
38
+ expired = true;
39
+ }
40
+ }
41
+ isLoading = false;
42
+ }
43
+
44
+ load();
45
+
46
+ // Drops the stored token so whatever the shopper does next starts a fresh cart, and clears bw-cart's
47
+ // bar/button and any host-side summary built from bw:cart-updated - the cart they were describing no longer
48
+ // exists. Mirrors what onOrderConfirmed below does once a cart has served its purpose the other way.
49
+ function onCartExpired() {
50
+ cartManager?.reset();
51
+ postMessage({ type: 'cart:updated', cart: null });
52
+ postMessage({ type: 'cart:expired' });
53
+ }
54
+
55
+ // In modal mode, modal:close is what actually dismisses this screen - bw-checkout.svelte remounts this
56
+ // component fresh the next time it opens, so the click is fully handled there. Rendered permanently
57
+ // in-page instead, nothing is listening for modal:close, so without the reload below the click would do
58
+ // nothing a shopper can see: expired flips off, but cart is still the stale null from the load() that
59
+ // found it gone, leaving the same "No items in cart" text up with no sign anything happened.
60
+ function startAgain() {
61
+ expired = false;
62
+ postMessage({ type: 'modal:close' });
63
+ void load();
64
+ }
65
+
66
+ // In modal mode this is redundant - bw-checkout.svelte only mounts this component fresh each time the
67
+ // modal opens, so it already gets a current cart. Rendered permanently in-page instead (no is-modal), it
68
+ // would otherwise only ever see the cart as it was on first mount. The event already carries the fresh
69
+ // cart (see TicketConfigurator/CheckoutModal's own posting sites), so this applies it directly rather than
70
+ // triggering a second, redundant getCart() call or flashing the spinner over an already-visible cart.
71
+ $effect(() => onWidgetMessage((d) => {
72
+ if (d.type === 'cart:updated' && 'cart' in d) cart = d.cart as CheckoutCartDetailDto | null;
73
+ }));
74
+ </script>
75
+
76
+ <div class="bw-widget">
77
+ {#if isLoading}
78
+ <div class="loading-center" style="height:100vh">
79
+ <div class="spinner"></div>
80
+ </div>
81
+ {:else if cart && cart.items.length > 0}
82
+ <CheckoutModal
83
+ {cart}
84
+ {api}
85
+ {wizardPages}
86
+ {editPages}
87
+ {autoSelectSingleTimeSlot}
88
+ onClose={() => postMessage({ type: 'modal:close' })}
89
+ onOrderConfirmed={() => {
90
+ const total = cart!.items.reduce((s, i) => s + i.amount, 0);
91
+ const currency = cart!.items[0]?.currencyCode ?? 'ZAR';
92
+ postMessage({
93
+ type: 'order:complete',
94
+ cartToken: cart!.cartToken ?? '',
95
+ value: total,
96
+ currency,
97
+ });
98
+ // The cart is paid and confirmed, so its token has done its job. Nothing used to clear it, so it sat
99
+ // in localStorage on the merchant's origin until absoluteExpiresAt - readable by every third-party
100
+ // script they load, and picked up by the next person to use a shared or kiosk browser.
101
+ cartManager?.reset();
102
+ }}
103
+ />
104
+ {:else if expired}
105
+ <div style="height:100vh">
106
+ <CartExpiredView onStartAgain={startAgain} />
107
+ </div>
108
+ {:else}
109
+ <div class="loading-center" style="height:100vh;color:var(--bw-color-text-secondary)">
110
+ No items in cart.
111
+ </div>
112
+ {/if}
113
+ </div>
114
+
115
+ <style>
116
+ /* display: contents - a plain box here would break CheckoutModal's own .modal{height:100%}, which needs
117
+ to resolve against this component's real parent, not an unsized wrapper inserted in between. */
118
+ .bw-widget {
119
+ display: contents;
120
+ }
121
+ </style>