@code-collective/booking-widget 1.0.9 → 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
package/package.json
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@code-collective/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "dist/booking-widget.umd.cjs",
|
|
6
|
-
"module": "dist/booking-widget.js",
|
|
7
|
-
"svelte": "src/lib/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"svelte": "./src/lib/index.ts",
|
|
11
|
-
"import": "./dist/booking-widget.js",
|
|
12
|
-
"require": "./dist/booking-widget.umd.cjs"
|
|
13
|
-
},
|
|
14
|
-
"./style.css": "./dist/booking-widget.css",
|
|
15
|
-
"./elements": "./dist/booking-widget.min.js"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"dist",
|
|
19
|
-
"src/lib"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"svelte": "^5.
|
|
39
|
-
"svelte
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
},
|
|
47
|
-
"
|
|
48
|
-
"svelte":
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@code-collective/booking-widget",
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/booking-widget.umd.cjs",
|
|
6
|
+
"module": "dist/booking-widget.js",
|
|
7
|
+
"svelte": "src/lib/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"svelte": "./src/lib/index.ts",
|
|
11
|
+
"import": "./dist/booking-widget.js",
|
|
12
|
+
"require": "./dist/booking-widget.umd.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./style.css": "./dist/booking-widget.css",
|
|
15
|
+
"./elements": "./dist/booking-widget.min.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/lib",
|
|
20
|
+
"!src/lib/**/*.test.ts",
|
|
21
|
+
"!src/lib/test",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "vite",
|
|
26
|
+
"build": "vite build",
|
|
27
|
+
"prepublishOnly": "npm run build && npm run build:elements",
|
|
28
|
+
"build:elements": "vite build --mode elements",
|
|
29
|
+
"postbuild:elements": "node -e \"const{readFileSync:r}=require('fs'),{createHash:h}=require('crypto');['dist/booking-widget.min.js','dist/booking-widget.min.css'].forEach(f=>{const d=h('sha384').update(r(f)).digest('base64');console.log(f+' -> sha384-'+d)})\"",
|
|
30
|
+
"build:demo": "vite build --mode demo",
|
|
31
|
+
"preview": "vite preview",
|
|
32
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"release": "node scripts/publish.cjs"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
38
|
+
"@testing-library/svelte": "^5.4.2",
|
|
39
|
+
"@tsconfig/svelte": "^5.0.0",
|
|
40
|
+
"jsdom": "^30.0.1",
|
|
41
|
+
"svelte": "^5.0.0",
|
|
42
|
+
"svelte-check": "^4.0.0",
|
|
43
|
+
"typescript": "^5.5.0",
|
|
44
|
+
"vite": "^8.2.2",
|
|
45
|
+
"vitest": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"svelte": "^5.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"svelte": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"openapi-fetch": "^0.17.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Puts a host in context for everything beneath it. The host itself is still built by the consumer -
|
|
3
|
+
// createBookingHost() works anywhere, including a module shared across several Astro islands, and this
|
|
4
|
+
// only decides which components can see it.
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import type { BookingHost } from './host.svelte';
|
|
7
|
+
import { setBookingHostContext } from './booking-context';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
host: BookingHost;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
}
|
|
13
|
+
let { host, children }: Props = $props();
|
|
14
|
+
|
|
15
|
+
// Read once, deliberately: context is set at initialisation and a provider serves one host for its
|
|
16
|
+
// lifetime. Swapping hosts mid-flight would strand every component already reading the old one.
|
|
17
|
+
// svelte-ignore state_referenced_locally
|
|
18
|
+
setBookingHostContext(host);
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{@render children()}
|
package/src/lib/CartBar.svelte
CHANGED
|
@@ -1,41 +1,26 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BookingApi } from './api';
|
|
3
|
-
import type {
|
|
4
|
-
import { CartManager } from './cart-manager';
|
|
5
|
-
import {
|
|
6
|
-
import CartBarView from './CartBarView.svelte';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
load();
|
|
29
|
-
|
|
30
|
-
// load() above only runs once on mount - without this, adding/editing/removing an item elsewhere on the
|
|
31
|
-
// page (the configurator, or the checkout modal's own edit/remove) leaves this bar showing a stale total.
|
|
32
|
-
// The event already carries the fresh cart (see TicketConfigurator/CheckoutModal's own posting sites), so
|
|
33
|
-
// this applies it directly rather than triggering a second, redundant getCart() call.
|
|
34
|
-
$effect(() => onWidgetMessage((d) => {
|
|
35
|
-
if (d.type === 'cart:updated' && 'cart' in d) cart = d.cart as CheckoutCartDetailDto | null;
|
|
36
|
-
}));
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
{#if !isLoading}
|
|
40
|
-
<CartBarView {cart} />
|
|
41
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BookingApi } from './api';
|
|
3
|
+
import type { CartOverviewState } from './config';
|
|
4
|
+
import { CartManager } from './cart-manager';
|
|
5
|
+
import { createCartOverview } from './cart-overview.svelte';
|
|
6
|
+
import CartBarView from './CartBarView.svelte';
|
|
7
|
+
import { getBookingHostContext, requireBookingService } from './booking-context';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
api?: BookingApi;
|
|
11
|
+
cartManager?: CartManager;
|
|
12
|
+
// Lets a host render its own summary against the same state the bar is in, rather than inferring it from
|
|
13
|
+
// an empty DOM - see cart-overview.svelte.ts.
|
|
14
|
+
onStateChange?: (state: CartOverviewState) => void;
|
|
15
|
+
}
|
|
16
|
+
let { api, cartManager, onStateChange }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const bookingHost = getBookingHostContext();
|
|
19
|
+
const overview = createCartOverview(
|
|
20
|
+
() => requireBookingService(api ?? bookingHost?.api, 'CartBar', 'api'),
|
|
21
|
+
() => requireBookingService(cartManager ?? bookingHost?.cartManager, 'CartBar', 'cartManager'),
|
|
22
|
+
(s) => onStateChange?.(s),
|
|
23
|
+
);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<CartBarView cart={overview.cart} state={overview.state} onRetry={overview.reload} />
|
|
@@ -1,61 +1,78 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { CheckoutCartDetailDto } from './client-types';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CheckoutCartDetailDto } from './client-types';
|
|
3
|
+
import type { CartOverviewState } from './config';
|
|
4
|
+
import { formatCurrency } from './currency';
|
|
5
|
+
import { postMessage } from './messages';
|
|
6
|
+
import CountdownTimer from './CountdownTimer.svelte';
|
|
7
|
+
import { cartDeadline } from './cart-expiry';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
cart: CheckoutCartDetailDto | null;
|
|
11
|
+
state?: CartOverviewState;
|
|
12
|
+
onRetry?: () => void;
|
|
13
|
+
}
|
|
14
|
+
let { cart, state = 'ready', onRetry }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let itemCount = $derived(cart?.items.length ?? 0);
|
|
17
|
+
|
|
18
|
+
let totalLabel = $derived(() => {
|
|
19
|
+
if (!cart || cart.items.length === 0) return '';
|
|
20
|
+
const currency = cart.items[0].currencyCode;
|
|
21
|
+
const total = cart.items.reduce((sum, i) => sum + i.amount, 0);
|
|
22
|
+
return formatCurrency(total, currency, 2, 2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let expiresAt = $derived(() => cart ? cartDeadline(cart) : new Date());
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
{#if state === 'error'}
|
|
29
|
+
<!-- An empty cart and a cart that could not be read used to render identically (as nothing), which left a
|
|
30
|
+
shopper on a narrow layout with no route to checkout and no sign that anything had gone wrong. -->
|
|
31
|
+
<div class="cart-bar bw-widget" role="alert">
|
|
32
|
+
<div class="cart-info">
|
|
33
|
+
<span class="item-count">Cart unavailable</span>
|
|
34
|
+
<span class="error-detail">We could not load your cart just now.</span>
|
|
35
|
+
</div>
|
|
36
|
+
<button class="btn btn-primary" onclick={() => onRetry?.()}>Retry</button>
|
|
37
|
+
</div>
|
|
38
|
+
{:else if cart && cart.items.length > 0}
|
|
39
|
+
<div class="cart-bar bw-widget">
|
|
40
|
+
<div class="cart-info">
|
|
41
|
+
<span class="item-count">{itemCount === 1 ? '1 item' : `${itemCount} items`} <CountdownTimer expiresAt={expiresAt()} /></span>
|
|
42
|
+
<span class="total">{totalLabel()}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<button class="btn btn-primary" onclick={() => postMessage({ type: 'modal:open' })}>
|
|
45
|
+
Checkout
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.cart-bar {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
padding: 12px 16px;
|
|
56
|
+
background: white;
|
|
57
|
+
border-top: 1px solid var(--bw-color-border);
|
|
58
|
+
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
61
|
+
.cart-info {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 2px;
|
|
65
|
+
}
|
|
66
|
+
.item-count {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
color: var(--bw-color-text-secondary);
|
|
69
|
+
}
|
|
70
|
+
.error-detail {
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
.total {
|
|
75
|
+
font-size: 16px;
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -16,15 +16,22 @@
|
|
|
16
16
|
import CartExpiryWatcher from './CartExpiryWatcher.svelte';
|
|
17
17
|
import StillTherePrompt from './StillTherePrompt.svelte';
|
|
18
18
|
import CartExpiredView from './CartExpiredView.svelte';
|
|
19
|
+
import { getBookingHostContext, requireBookingService } from './booking-context';
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
|
-
api
|
|
22
|
-
cartManager
|
|
22
|
+
api?: BookingApi;
|
|
23
|
+
cartManager?: CartManager;
|
|
23
24
|
// Whether the "are you still there?" prompt is offered - see EXTENSION_ENABLED. Off, the guard only ever
|
|
24
25
|
// shows the expired view at 0:00; the clock itself is unchanged either way.
|
|
25
26
|
extensionEnabled?: boolean;
|
|
26
27
|
}
|
|
27
|
-
let { api, cartManager, extensionEnabled = EXTENSION_ENABLED }: Props = $props();
|
|
28
|
+
let { api: apiProp, cartManager: cartManagerProp, extensionEnabled = EXTENSION_ENABLED }: Props = $props();
|
|
29
|
+
|
|
30
|
+
const bookingHost = getBookingHostContext();
|
|
31
|
+
let api = $derived(requireBookingService(apiProp ?? bookingHost?.api, 'CartExpiryGuard', 'api'));
|
|
32
|
+
let cartManager = $derived(
|
|
33
|
+
requireBookingService(cartManagerProp ?? bookingHost?.cartManager, 'CartExpiryGuard', 'cartManager'),
|
|
34
|
+
);
|
|
28
35
|
|
|
29
36
|
let cart = $state<CheckoutCartDetailDto | null>(null);
|
|
30
37
|
let remaining = $state('');
|
|
@@ -322,14 +329,13 @@
|
|
|
322
329
|
clearTimers();
|
|
323
330
|
closed = true;
|
|
324
331
|
cartManager.reset();
|
|
325
|
-
// Posted first so a listener that cares about expiry specifically (register.ts's bw:cart-expired) hears
|
|
326
|
-
// it before the more general cart:updated below - order does not matter to either side today, but this
|
|
327
|
-
// keeps the more specific signal from ever reading as a follow-up to the general one.
|
|
328
|
-
postMessage({ type: 'cart:expired' });
|
|
329
332
|
// Tells bw-cart/CartOverviewButton/CheckoutModal (if it happens to be mounted) the cart is gone, the same
|
|
330
|
-
// signal a manual remove-to-empty already sends - see
|
|
331
|
-
// caller of this exact message.
|
|
333
|
+
// signal a manual remove-to-empty already sends - see CheckoutPanel's own onCartExpired for the other
|
|
334
|
+
// caller of this exact message. cart:expired follows it because a null cart alone does not say why: a
|
|
335
|
+
// confirmed order clears the cart the same way, and a host announcing "your cart expired" needs the two
|
|
336
|
+
// apart.
|
|
332
337
|
postMessage({ type: 'cart:updated', cart: null });
|
|
338
|
+
postMessage({ type: 'cart:expired' });
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
function startAgain() {
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BookingApi } from './api';
|
|
3
|
-
import type { CartOverviewDisplay } from './config';
|
|
4
|
-
import { CartManager } from './cart-manager';
|
|
5
|
-
import CartBar from './CartBar.svelte';
|
|
6
|
-
import CartOverviewButton from './CartOverviewButton.svelte';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BookingApi } from './api';
|
|
3
|
+
import type { CartOverviewDisplay, CartOverviewState } from './config';
|
|
4
|
+
import { CartManager } from './cart-manager';
|
|
5
|
+
import CartBar from './CartBar.svelte';
|
|
6
|
+
import CartOverviewButton from './CartOverviewButton.svelte';
|
|
7
|
+
import { getBookingHostContext, requireBookingService } from './booking-context';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
api?: BookingApi;
|
|
11
|
+
cartManager?: CartManager;
|
|
12
|
+
display: CartOverviewDisplay;
|
|
13
|
+
onStateChange?: (state: CartOverviewState) => void;
|
|
14
|
+
}
|
|
15
|
+
let { api, cartManager, display, onStateChange }: Props = $props();
|
|
16
|
+
|
|
17
|
+
// Resolved here as well as in the children so a missing host names CartOverview, which is what the
|
|
18
|
+
// consumer actually wrote, rather than CartBar.
|
|
19
|
+
const bookingHost = getBookingHostContext();
|
|
20
|
+
let resolvedApi = $derived(requireBookingService(api ?? bookingHost?.api, 'CartOverview', 'api'));
|
|
21
|
+
let resolvedCartManager = $derived(
|
|
22
|
+
requireBookingService(cartManager ?? bookingHost?.cartManager, 'CartOverview', 'cartManager'),
|
|
23
|
+
);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{#if display === 'button'}
|
|
27
|
+
<CartOverviewButton api={resolvedApi} cartManager={resolvedCartManager} {onStateChange} />
|
|
28
|
+
{:else}
|
|
29
|
+
<CartBar api={resolvedApi} cartManager={resolvedCartManager} {onStateChange} />
|
|
30
|
+
{/if}
|