@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.
- package/CHANGELOG.md +48 -46
- package/README.md +500 -500
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +829 -707
- package/dist/booking-widget.min.js +24 -25
- package/dist/booking-widget.umd.cjs +5 -5
- package/package.json +58 -58
- package/src/lib/BookingProvider.svelte +21 -21
- package/src/lib/CartBar.svelte +26 -26
- package/src/lib/CartBarView.svelte +78 -78
- package/src/lib/CartExpiryGuard.svelte +416 -416
- package/src/lib/CartOverview.svelte +30 -30
- package/src/lib/CartOverviewButton.svelte +104 -104
- package/src/lib/Checkout.svelte +141 -141
- package/src/lib/CheckoutModal.css +179 -0
- package/src/lib/CheckoutModal.svelte +78 -573
- package/src/lib/CheckoutPanel.svelte +121 -121
- package/src/lib/PaymentPage.svelte +208 -191
- package/src/lib/ResultView.svelte +24 -4
- package/src/lib/TicketConfigurator.svelte +166 -166
- package/src/lib/api.ts +36 -5
- package/src/lib/booking-context.ts +33 -33
- package/src/lib/cart-overview.svelte.ts +97 -97
- package/src/lib/checkout-item-view.ts +63 -0
- package/src/lib/checkout-payment-flow.svelte.ts +523 -0
- package/src/lib/client-types.ts +22 -6
- package/src/lib/config.ts +162 -162
- package/src/lib/elements/bw-cart.svelte +49 -49
- package/src/lib/elements/bw-checkout.svelte +97 -97
- package/src/lib/elements/bw-configurator.svelte +72 -72
- package/src/lib/elements/register.ts +171 -171
- package/src/lib/elements/shared.ts +18 -18
- package/src/lib/generated-types.ts +94 -5
- package/src/lib/host.svelte.ts +336 -336
- package/src/lib/index.ts +242 -242
- package/src/lib/messages.ts +157 -157
- package/src/lib/peach-sdk.ts +86 -86
- 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>
|