@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,97 +1,97 @@
1
- // The cart state behind both CartOverview presentations. Extracted because the bar and the button had the
2
- // same load-then-follow-cart:updated logic written twice, and both answered a failed fetch the same wrong
3
- // way: `cart = null`, rendering exactly what an empty cart renders - nothing at all. On a narrow layout the
4
- // bar is the only route to checkout, so "no DOM" has to mean something a consumer can act on.
5
-
6
- import type { BookingApi } from './api';
7
- import type { CartManager } from './cart-manager';
8
- import type { CartOverviewState } from './config';
9
- import type { CheckoutCartDetailDto } from './client-types';
10
- import { onWidgetMessage } from './messages';
11
-
12
- export interface CartOverview {
13
- readonly cart: CheckoutCartDetailDto | null;
14
- readonly state: CartOverviewState;
15
- /** Re-reads the cart. What the error presentation's retry is wired to. */
16
- readonly reload: () => void;
17
- }
18
-
19
- /**
20
- * Call during component initialisation - it registers an $effect that follows cart:updated for the
21
- * component's lifetime.
22
- */
23
- export function createCartOverview(
24
- // Accessors rather than values: these arrive as props, and reading them through a closure keeps this
25
- // following the current ones rather than pinning whichever pair happened to be passed on first render.
26
- getApi: () => BookingApi,
27
- getCartManager: () => CartManager,
28
- onStateChange?: (state: CartOverviewState) => void,
29
- ): CartOverview {
30
- let cart = $state<CheckoutCartDetailDto | null>(null);
31
- let failed = $state(false);
32
- let isLoading = $state(true);
33
- // Only the newest load may write. A read still in flight when a fresher cart arrives - from a retry, or
34
- // from cart:updated after an add elsewhere on the page - would otherwise land afterwards and overwrite it,
35
- // and a transient failure landing late would replace a perfectly good cart summary with the error view.
36
- let currentLoad = 0;
37
-
38
- async function load() {
39
- const thisLoad = ++currentLoad;
40
- isLoading = true;
41
- failed = false;
42
- // No token held means there is genuinely nothing to fetch - an empty cart, not a failure.
43
- if (!getCartManager().hasCart) {
44
- cart = null;
45
- isLoading = false;
46
- return;
47
- }
48
- try {
49
- const loaded = await getApi().getCart();
50
- if (thisLoad !== currentLoad) return;
51
- cart = loaded;
52
- } catch {
53
- if (thisLoad !== currentLoad) return;
54
- cart = null;
55
- failed = true;
56
- }
57
- isLoading = false;
58
- }
59
-
60
- void load();
61
-
62
- // load() only runs once - without this, adding/editing/removing an item elsewhere on the page (the
63
- // configurator, or the checkout modal's own edit/remove) leaves this showing a stale total. The event
64
- // already carries the fresh cart (see TicketConfigurator/CheckoutModal's own posting sites), so it is
65
- // applied directly rather than triggering a second, redundant getCart() call.
66
- $effect(() =>
67
- onWidgetMessage((d) => {
68
- if (d.type === 'cart:updated') {
69
- // Supersedes any read still in flight - this cart is newer than whatever it will return.
70
- currentLoad += 1;
71
- cart = d.cart;
72
- // A cart arriving by message is a cart successfully read somewhere else on the page, so whatever
73
- // this one's own fetch ran into is no longer the current answer.
74
- failed = false;
75
- isLoading = false;
76
- }
77
- }),
78
- );
79
-
80
- const state = $derived<CartOverviewState>(
81
- isLoading ? 'loading' : failed ? 'error' : (cart?.items?.length ?? 0) > 0 ? 'ready' : 'empty',
82
- );
83
-
84
- $effect(() => {
85
- onStateChange?.(state);
86
- });
87
-
88
- return {
89
- get cart() {
90
- return cart;
91
- },
92
- get state() {
93
- return state;
94
- },
95
- reload: () => void load(),
96
- };
97
- }
1
+ // The cart state behind both CartOverview presentations. Extracted because the bar and the button had the
2
+ // same load-then-follow-cart:updated logic written twice, and both answered a failed fetch the same wrong
3
+ // way: `cart = null`, rendering exactly what an empty cart renders - nothing at all. On a narrow layout the
4
+ // bar is the only route to checkout, so "no DOM" has to mean something a consumer can act on.
5
+
6
+ import type { BookingApi } from './api';
7
+ import type { CartManager } from './cart-manager';
8
+ import type { CartOverviewState } from './config';
9
+ import type { CheckoutCartDetailDto } from './client-types';
10
+ import { onWidgetMessage } from './messages';
11
+
12
+ export interface CartOverview {
13
+ readonly cart: CheckoutCartDetailDto | null;
14
+ readonly state: CartOverviewState;
15
+ /** Re-reads the cart. What the error presentation's retry is wired to. */
16
+ readonly reload: () => void;
17
+ }
18
+
19
+ /**
20
+ * Call during component initialisation - it registers an $effect that follows cart:updated for the
21
+ * component's lifetime.
22
+ */
23
+ export function createCartOverview(
24
+ // Accessors rather than values: these arrive as props, and reading them through a closure keeps this
25
+ // following the current ones rather than pinning whichever pair happened to be passed on first render.
26
+ getApi: () => BookingApi,
27
+ getCartManager: () => CartManager,
28
+ onStateChange?: (state: CartOverviewState) => void,
29
+ ): CartOverview {
30
+ let cart = $state<CheckoutCartDetailDto | null>(null);
31
+ let failed = $state(false);
32
+ let isLoading = $state(true);
33
+ // Only the newest load may write. A read still in flight when a fresher cart arrives - from a retry, or
34
+ // from cart:updated after an add elsewhere on the page - would otherwise land afterwards and overwrite it,
35
+ // and a transient failure landing late would replace a perfectly good cart summary with the error view.
36
+ let currentLoad = 0;
37
+
38
+ async function load() {
39
+ const thisLoad = ++currentLoad;
40
+ isLoading = true;
41
+ failed = false;
42
+ // No token held means there is genuinely nothing to fetch - an empty cart, not a failure.
43
+ if (!getCartManager().hasCart) {
44
+ cart = null;
45
+ isLoading = false;
46
+ return;
47
+ }
48
+ try {
49
+ const loaded = await getApi().getCart();
50
+ if (thisLoad !== currentLoad) return;
51
+ cart = loaded;
52
+ } catch {
53
+ if (thisLoad !== currentLoad) return;
54
+ cart = null;
55
+ failed = true;
56
+ }
57
+ isLoading = false;
58
+ }
59
+
60
+ void load();
61
+
62
+ // load() only runs once - without this, adding/editing/removing an item elsewhere on the page (the
63
+ // configurator, or the checkout modal's own edit/remove) leaves this showing a stale total. The event
64
+ // already carries the fresh cart (see TicketConfigurator/CheckoutModal's own posting sites), so it is
65
+ // applied directly rather than triggering a second, redundant getCart() call.
66
+ $effect(() =>
67
+ onWidgetMessage((d) => {
68
+ if (d.type === 'cart:updated') {
69
+ // Supersedes any read still in flight - this cart is newer than whatever it will return.
70
+ currentLoad += 1;
71
+ cart = d.cart;
72
+ // A cart arriving by message is a cart successfully read somewhere else on the page, so whatever
73
+ // this one's own fetch ran into is no longer the current answer.
74
+ failed = false;
75
+ isLoading = false;
76
+ }
77
+ }),
78
+ );
79
+
80
+ const state = $derived<CartOverviewState>(
81
+ isLoading ? 'loading' : failed ? 'error' : (cart?.items?.length ?? 0) > 0 ? 'ready' : 'empty',
82
+ );
83
+
84
+ $effect(() => {
85
+ onStateChange?.(state);
86
+ });
87
+
88
+ return {
89
+ get cart() {
90
+ return cart;
91
+ },
92
+ get state() {
93
+ return state;
94
+ },
95
+ reload: () => void load(),
96
+ };
97
+ }
@@ -0,0 +1,63 @@
1
+ // Pure view-model helpers for rendering a CheckoutCartDetailDto's items - split out of CheckoutModal.svelte
2
+ // because none of these hold or need reactive state of their own; they are plain functions of the item (and,
3
+ // for the ones that need titles/units, the already-loaded product catalog), called straight from the
4
+ // template on every render.
5
+
6
+ import type { CheckoutCartDetailDto, CheckoutCartItemDetailDto, CheckoutProductDto } from './client-types';
7
+
8
+ export function cartTotal(cart: CheckoutCartDetailDto): number {
9
+ return cart.items.reduce((s, i) => s + i.amount, 0);
10
+ }
11
+
12
+ // Every item in one cart shares a currency (PayCheckoutCartCommandHandler refuses to mix them), so the first
13
+ // item's is the cart's - defaulting to ZAR only for the moment before the first item exists.
14
+ export function cartCurrency(cart: CheckoutCartDetailDto): string {
15
+ return cart.items[0]?.currencyCode ?? 'ZAR';
16
+ }
17
+
18
+ export function productTitle(item: CheckoutCartItemDetailDto, productsById: Map<string, CheckoutProductDto>): string {
19
+ return productsById.get(item.productId)?.title ?? item.productId;
20
+ }
21
+
22
+ export function optionTitle(item: CheckoutCartItemDetailDto, productsById: Map<string, CheckoutProductDto>): string {
23
+ const product = productsById.get(item.productId);
24
+ return product?.options.find((o) => o.id === item.optionId)?.title ?? item.optionId;
25
+ }
26
+
27
+ export interface GroupedUnit {
28
+ unitId: string;
29
+ title: string;
30
+ quantity: number;
31
+ linePrice: number;
32
+ }
33
+
34
+ export function groupUnits(
35
+ item: CheckoutCartItemDetailDto,
36
+ productsById: Map<string, CheckoutProductDto>,
37
+ ): GroupedUnit[] {
38
+ const product = productsById.get(item.productId);
39
+ const option = product?.options.find((o) => o.id === item.optionId);
40
+ const counts = new Map<string, number>();
41
+ for (const u of item.unitItems) {
42
+ counts.set(u.unitId, (counts.get(u.unitId) ?? 0) + 1);
43
+ }
44
+ return [...counts.entries()].map(([unitId, quantity]) => {
45
+ const unit = option?.units.find((u) => u.id === unitId);
46
+ const unitPrice = unit?.pricing?.[0]?.retail ?? 0;
47
+ return {
48
+ unitId,
49
+ title: unit?.title ?? unitId,
50
+ quantity,
51
+ linePrice: unitPrice * quantity,
52
+ };
53
+ });
54
+ }
55
+
56
+ export function itemDateLabel(item: CheckoutCartItemDetailDto): string | null {
57
+ if (!item.availabilityId) return null;
58
+ const match = item.availabilityId.match(/(\d{4})-(\d{2})-(\d{2})/);
59
+ if (!match) return null;
60
+ const d = new Date(+match[1], +match[2] - 1, +match[3]);
61
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
62
+ return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
63
+ }