@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,166 +1,166 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BookingApi } from './api';
|
|
3
|
-
import { ApiError, isPriceMismatch } from './api';
|
|
4
|
-
import type { WizardPages } from './config';
|
|
5
|
-
import { DEFAULT_WIZARD_PAGES } from './config';
|
|
6
|
-
import type { CartItem, CheckoutProductDto } from './client-types';
|
|
7
|
-
import { CartManager } from './cart-manager';
|
|
8
|
-
import { postMessage } from './messages';
|
|
9
|
-
import { expandUnitItems } from './utils';
|
|
10
|
-
import { formatCurrency } from './currency';
|
|
11
|
-
import WizardPage from './WizardPage.svelte';
|
|
12
|
-
import { getBookingHostContext, requireBookingService } from './booking-context';
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
// Optional when a BookingProvider is above this component - it supplies both from its host.
|
|
16
|
-
api?: BookingApi;
|
|
17
|
-
cartManager?: CartManager;
|
|
18
|
-
productId: string;
|
|
19
|
-
wizardPages?: WizardPages;
|
|
20
|
-
autoSelectSingleTimeSlot?: boolean;
|
|
21
|
-
// Whether an add from this configurator asks to be carried on into checkout. Stated on the cart:change
|
|
22
|
-
// message rather than acted on here, so the host decides how to honour it - and so bw-configurator's
|
|
23
|
-
// no-auto-checkout attribute and a host's own autoOpenCheckout: false meet in the same branch (see
|
|
24
|
-
// host.svelte.ts) instead of each build having its own way to suppress it.
|
|
25
|
-
autoOpenCheckout?: boolean;
|
|
26
|
-
onCancel?: () => void;
|
|
27
|
-
}
|
|
28
|
-
let { api: apiProp, cartManager: cartManagerProp, productId, wizardPages = DEFAULT_WIZARD_PAGES,
|
|
29
|
-
autoSelectSingleTimeSlot = false, autoOpenCheckout = true, onCancel }: Props = $props();
|
|
30
|
-
|
|
31
|
-
const bookingHost = getBookingHostContext();
|
|
32
|
-
let api = $derived(requireBookingService(apiProp ?? bookingHost?.api, 'TicketConfigurator', 'api'));
|
|
33
|
-
let cartManager = $derived(
|
|
34
|
-
requireBookingService(cartManagerProp ?? bookingHost?.cartManager, 'TicketConfigurator', 'cartManager'),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
let product = $state<CheckoutProductDto | null>(null);
|
|
38
|
-
let isLoading = $state(true);
|
|
39
|
-
let isAddingToCart = $state(false);
|
|
40
|
-
let addToCartError = $state<string | null>(null);
|
|
41
|
-
|
|
42
|
-
async function load() {
|
|
43
|
-
product = await api.getProduct(productId);
|
|
44
|
-
isLoading = false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
load();
|
|
48
|
-
|
|
49
|
-
async function addItemToCart(item: CartItem): Promise<void> {
|
|
50
|
-
await cartManager.ensureCart();
|
|
51
|
-
|
|
52
|
-
const unitItems = expandUnitItems(item.units);
|
|
53
|
-
const added = await api.addCartItem({
|
|
54
|
-
productId: item.productId,
|
|
55
|
-
optionId: item.optionId,
|
|
56
|
-
unitItems,
|
|
57
|
-
availabilityId: item.availabilityId,
|
|
58
|
-
localDate: item.localDate,
|
|
59
|
-
pickupPointId: item.pickupPointId,
|
|
60
|
-
// Currency and precision are the supplier's to state, not ours - the API resolves both server-side and
|
|
61
|
-
// ignores anything sent here. Amount stays, but only as a claim about the price the customer was shown:
|
|
62
|
-
// the server accepts it solely when it matches what the supplier is asking or a price the server itself
|
|
63
|
-
// published, and returns PRICE_MISMATCH otherwise.
|
|
64
|
-
amount: item.totalPrice,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const total = formatCurrency(item.totalPrice, item.currency, item.currencyPrecision, 2);
|
|
68
|
-
postMessage({
|
|
69
|
-
type: 'cart:change',
|
|
70
|
-
itemCount: unitItems.length,
|
|
71
|
-
cartItemId: added.id ?? '',
|
|
72
|
-
totalFormatted: total,
|
|
73
|
-
openCheckout: autoOpenCheckout,
|
|
74
|
-
});
|
|
75
|
-
// Separate from cart:change above - that one is per-item-added detail forwarded to consumers as the
|
|
76
|
-
// public bw:cart-change event. This one carries the fresh cart itself: CartBar/CartOverviewButton/Checkout
|
|
77
|
-
// use it to update their own state without each independently re-fetching, and it's forwarded to
|
|
78
|
-
// consumers as the public bw:cart-updated event/onCartUpdated callback - see this event's own doc comment
|
|
79
|
-
// in CheckoutModal (the other place it's posted from, after an edit/remove).
|
|
80
|
-
const cart = await api.getCart().catch(() => null);
|
|
81
|
-
postMessage({ type: 'cart:updated', cart });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function onAddToCart(item: CartItem) {
|
|
85
|
-
isAddingToCart = true;
|
|
86
|
-
addToCartError = null;
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
await addItemToCart(item);
|
|
90
|
-
} catch (e) {
|
|
91
|
-
// The supplier moved the price while the customer was configuring, and the amount they agreed to is no
|
|
92
|
-
// longer one the server accepts. Retrying is useless - the wizard still holds the old prices, so it
|
|
93
|
-
// would resubmit exactly the same amount and fail identically. The only way out is to re-fetch and let
|
|
94
|
-
// the customer see the new price, which is what the message asks them to do.
|
|
95
|
-
if (isPriceMismatch(e)) {
|
|
96
|
-
await reloadAfterPriceChange();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// CartManager only knows a cart is expired from timestamps it saw at creation - it can't see the
|
|
101
|
-
// server's idle window sliding forward, so a cart that looks valid locally can still be rejected
|
|
102
|
-
// server-side. A 401/404 here means exactly that: drop the stale cart and retry once with a fresh one
|
|
103
|
-
// before giving up, rather than leaving the user stuck with no feedback and no way to proceed.
|
|
104
|
-
const isStaleCart = e instanceof ApiError && (e.status === 401 || e.status === 404);
|
|
105
|
-
if (isStaleCart) {
|
|
106
|
-
cartManager.reset();
|
|
107
|
-
try {
|
|
108
|
-
await addItemToCart(item);
|
|
109
|
-
} catch {
|
|
110
|
-
addToCartError = 'Something went wrong adding this to your cart. Please try again.';
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
addToCartError = 'Something went wrong adding this to your cart. Please try again.';
|
|
114
|
-
}
|
|
115
|
-
} finally {
|
|
116
|
-
isAddingToCart = false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function reloadAfterPriceChange() {
|
|
121
|
-
addToCartError = "This ticket's price has changed since you started. Please check the updated price and try again.";
|
|
122
|
-
try {
|
|
123
|
-
await load();
|
|
124
|
-
} catch {
|
|
125
|
-
// The reload is what makes the message actionable; if even that fails the customer needs to start over
|
|
126
|
-
// rather than be left looking at prices we already know are wrong.
|
|
127
|
-
addToCartError = "This ticket's price has changed and we could not load the new one. Please reload the page.";
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
</script>
|
|
131
|
-
|
|
132
|
-
<div class="bw-widget">
|
|
133
|
-
{#if isLoading || isAddingToCart}
|
|
134
|
-
<div class="loading-center" style="height:100vh">
|
|
135
|
-
<div class="spinner"></div>
|
|
136
|
-
</div>
|
|
137
|
-
{:else if product}
|
|
138
|
-
{#if addToCartError}
|
|
139
|
-
<p class="add-to-cart-error">{addToCartError}</p>
|
|
140
|
-
{/if}
|
|
141
|
-
<WizardPage
|
|
142
|
-
{product}
|
|
143
|
-
{api}
|
|
144
|
-
{wizardPages}
|
|
145
|
-
{autoSelectSingleTimeSlot}
|
|
146
|
-
{onCancel}
|
|
147
|
-
onComplete={onAddToCart}
|
|
148
|
-
/>
|
|
149
|
-
{/if}
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<style>
|
|
153
|
-
/* display: contents - a plain box here would break WizardPage's own .wizard{height:100%}, which needs
|
|
154
|
-
to resolve against this component's real parent, not an unsized wrapper inserted in between. */
|
|
155
|
-
.bw-widget {
|
|
156
|
-
display: contents;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.add-to-cart-error {
|
|
160
|
-
margin: 0;
|
|
161
|
-
padding: 12px 16px;
|
|
162
|
-
background: #fdecea;
|
|
163
|
-
color: #b3261e;
|
|
164
|
-
font-size: 14px;
|
|
165
|
-
}
|
|
166
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BookingApi } from './api';
|
|
3
|
+
import { ApiError, isPriceMismatch } from './api';
|
|
4
|
+
import type { WizardPages } from './config';
|
|
5
|
+
import { DEFAULT_WIZARD_PAGES } from './config';
|
|
6
|
+
import type { CartItem, CheckoutProductDto } from './client-types';
|
|
7
|
+
import { CartManager } from './cart-manager';
|
|
8
|
+
import { postMessage } from './messages';
|
|
9
|
+
import { expandUnitItems } from './utils';
|
|
10
|
+
import { formatCurrency } from './currency';
|
|
11
|
+
import WizardPage from './WizardPage.svelte';
|
|
12
|
+
import { getBookingHostContext, requireBookingService } from './booking-context';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
// Optional when a BookingProvider is above this component - it supplies both from its host.
|
|
16
|
+
api?: BookingApi;
|
|
17
|
+
cartManager?: CartManager;
|
|
18
|
+
productId: string;
|
|
19
|
+
wizardPages?: WizardPages;
|
|
20
|
+
autoSelectSingleTimeSlot?: boolean;
|
|
21
|
+
// Whether an add from this configurator asks to be carried on into checkout. Stated on the cart:change
|
|
22
|
+
// message rather than acted on here, so the host decides how to honour it - and so bw-configurator's
|
|
23
|
+
// no-auto-checkout attribute and a host's own autoOpenCheckout: false meet in the same branch (see
|
|
24
|
+
// host.svelte.ts) instead of each build having its own way to suppress it.
|
|
25
|
+
autoOpenCheckout?: boolean;
|
|
26
|
+
onCancel?: () => void;
|
|
27
|
+
}
|
|
28
|
+
let { api: apiProp, cartManager: cartManagerProp, productId, wizardPages = DEFAULT_WIZARD_PAGES,
|
|
29
|
+
autoSelectSingleTimeSlot = false, autoOpenCheckout = true, onCancel }: Props = $props();
|
|
30
|
+
|
|
31
|
+
const bookingHost = getBookingHostContext();
|
|
32
|
+
let api = $derived(requireBookingService(apiProp ?? bookingHost?.api, 'TicketConfigurator', 'api'));
|
|
33
|
+
let cartManager = $derived(
|
|
34
|
+
requireBookingService(cartManagerProp ?? bookingHost?.cartManager, 'TicketConfigurator', 'cartManager'),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
let product = $state<CheckoutProductDto | null>(null);
|
|
38
|
+
let isLoading = $state(true);
|
|
39
|
+
let isAddingToCart = $state(false);
|
|
40
|
+
let addToCartError = $state<string | null>(null);
|
|
41
|
+
|
|
42
|
+
async function load() {
|
|
43
|
+
product = await api.getProduct(productId);
|
|
44
|
+
isLoading = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
load();
|
|
48
|
+
|
|
49
|
+
async function addItemToCart(item: CartItem): Promise<void> {
|
|
50
|
+
await cartManager.ensureCart();
|
|
51
|
+
|
|
52
|
+
const unitItems = expandUnitItems(item.units);
|
|
53
|
+
const added = await api.addCartItem({
|
|
54
|
+
productId: item.productId,
|
|
55
|
+
optionId: item.optionId,
|
|
56
|
+
unitItems,
|
|
57
|
+
availabilityId: item.availabilityId,
|
|
58
|
+
localDate: item.localDate,
|
|
59
|
+
pickupPointId: item.pickupPointId,
|
|
60
|
+
// Currency and precision are the supplier's to state, not ours - the API resolves both server-side and
|
|
61
|
+
// ignores anything sent here. Amount stays, but only as a claim about the price the customer was shown:
|
|
62
|
+
// the server accepts it solely when it matches what the supplier is asking or a price the server itself
|
|
63
|
+
// published, and returns PRICE_MISMATCH otherwise.
|
|
64
|
+
amount: item.totalPrice,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const total = formatCurrency(item.totalPrice, item.currency, item.currencyPrecision, 2);
|
|
68
|
+
postMessage({
|
|
69
|
+
type: 'cart:change',
|
|
70
|
+
itemCount: unitItems.length,
|
|
71
|
+
cartItemId: added.id ?? '',
|
|
72
|
+
totalFormatted: total,
|
|
73
|
+
openCheckout: autoOpenCheckout,
|
|
74
|
+
});
|
|
75
|
+
// Separate from cart:change above - that one is per-item-added detail forwarded to consumers as the
|
|
76
|
+
// public bw:cart-change event. This one carries the fresh cart itself: CartBar/CartOverviewButton/Checkout
|
|
77
|
+
// use it to update their own state without each independently re-fetching, and it's forwarded to
|
|
78
|
+
// consumers as the public bw:cart-updated event/onCartUpdated callback - see this event's own doc comment
|
|
79
|
+
// in CheckoutModal (the other place it's posted from, after an edit/remove).
|
|
80
|
+
const cart = await api.getCart().catch(() => null);
|
|
81
|
+
postMessage({ type: 'cart:updated', cart });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function onAddToCart(item: CartItem) {
|
|
85
|
+
isAddingToCart = true;
|
|
86
|
+
addToCartError = null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await addItemToCart(item);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// The supplier moved the price while the customer was configuring, and the amount they agreed to is no
|
|
92
|
+
// longer one the server accepts. Retrying is useless - the wizard still holds the old prices, so it
|
|
93
|
+
// would resubmit exactly the same amount and fail identically. The only way out is to re-fetch and let
|
|
94
|
+
// the customer see the new price, which is what the message asks them to do.
|
|
95
|
+
if (isPriceMismatch(e)) {
|
|
96
|
+
await reloadAfterPriceChange();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// CartManager only knows a cart is expired from timestamps it saw at creation - it can't see the
|
|
101
|
+
// server's idle window sliding forward, so a cart that looks valid locally can still be rejected
|
|
102
|
+
// server-side. A 401/404 here means exactly that: drop the stale cart and retry once with a fresh one
|
|
103
|
+
// before giving up, rather than leaving the user stuck with no feedback and no way to proceed.
|
|
104
|
+
const isStaleCart = e instanceof ApiError && (e.status === 401 || e.status === 404);
|
|
105
|
+
if (isStaleCart) {
|
|
106
|
+
cartManager.reset();
|
|
107
|
+
try {
|
|
108
|
+
await addItemToCart(item);
|
|
109
|
+
} catch {
|
|
110
|
+
addToCartError = 'Something went wrong adding this to your cart. Please try again.';
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
addToCartError = 'Something went wrong adding this to your cart. Please try again.';
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
isAddingToCart = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function reloadAfterPriceChange() {
|
|
121
|
+
addToCartError = "This ticket's price has changed since you started. Please check the updated price and try again.";
|
|
122
|
+
try {
|
|
123
|
+
await load();
|
|
124
|
+
} catch {
|
|
125
|
+
// The reload is what makes the message actionable; if even that fails the customer needs to start over
|
|
126
|
+
// rather than be left looking at prices we already know are wrong.
|
|
127
|
+
addToCartError = "This ticket's price has changed and we could not load the new one. Please reload the page.";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<div class="bw-widget">
|
|
133
|
+
{#if isLoading || isAddingToCart}
|
|
134
|
+
<div class="loading-center" style="height:100vh">
|
|
135
|
+
<div class="spinner"></div>
|
|
136
|
+
</div>
|
|
137
|
+
{:else if product}
|
|
138
|
+
{#if addToCartError}
|
|
139
|
+
<p class="add-to-cart-error">{addToCartError}</p>
|
|
140
|
+
{/if}
|
|
141
|
+
<WizardPage
|
|
142
|
+
{product}
|
|
143
|
+
{api}
|
|
144
|
+
{wizardPages}
|
|
145
|
+
{autoSelectSingleTimeSlot}
|
|
146
|
+
{onCancel}
|
|
147
|
+
onComplete={onAddToCart}
|
|
148
|
+
/>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<style>
|
|
153
|
+
/* display: contents - a plain box here would break WizardPage's own .wizard{height:100%}, which needs
|
|
154
|
+
to resolve against this component's real parent, not an unsized wrapper inserted in between. */
|
|
155
|
+
.bw-widget {
|
|
156
|
+
display: contents;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.add-to-cart-error {
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 12px 16px;
|
|
162
|
+
background: #fdecea;
|
|
163
|
+
color: #b3261e;
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
package/src/lib/api.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
CheckoutCartItemAddedDto,
|
|
12
12
|
CheckoutCartPaymentInitiationDto,
|
|
13
13
|
CheckoutCartConfirmResultDto,
|
|
14
|
+
CheckoutCartStatusDto,
|
|
14
15
|
OctoContact,
|
|
15
16
|
} from './client-types';
|
|
16
17
|
import { dateKey } from './utils';
|
|
@@ -111,7 +112,16 @@ export interface BookingApi {
|
|
|
111
112
|
updateCartItem(itemId: string, request: CheckoutAddCartItemDto): Promise<CheckoutCartItemAddedDto>;
|
|
112
113
|
removeCartItem(itemId: string): Promise<void>;
|
|
113
114
|
payCart(contact: OctoContact): Promise<CheckoutCartPaymentInitiationDto>;
|
|
114
|
-
|
|
115
|
+
// Where the payment actually stands, as decided server-side off Peach's webhook. There is deliberately no
|
|
116
|
+
// confirmCart here: confirmation that depends on this tab staying open strands every shopper who closes it
|
|
117
|
+
// after paying, so the widget only ever reads the outcome, it never produces one.
|
|
118
|
+
getCartStatus(): Promise<CheckoutCartStatusDto>;
|
|
119
|
+
|
|
120
|
+
// Asks the server to find out what actually happened to the current attempt. Called once when Peach's form
|
|
121
|
+
// reports the checkout complete - "complete" covers a declined card as much as a charged one, and Peach
|
|
122
|
+
// sends no webhook for a decline - and then getCartStatus is polled for the answer. Best effort: the server
|
|
123
|
+
// answers 204 whatever it found, and the sweep reconciles anything it could not.
|
|
124
|
+
resolvePayment(): Promise<void>;
|
|
115
125
|
// Slides the cart's idle window out to a full window from now, clamped at its absolute ceiling, and
|
|
116
126
|
// re-extends the OCTO holds behind it. There is no refusal to handle: a cart already at its ceiling comes
|
|
117
127
|
// back with the deadline it already had (see isAtExpiryCeiling). Returns the cart with its new expiry so the
|
|
@@ -121,6 +131,11 @@ export interface BookingApi {
|
|
|
121
131
|
// used by CheckoutModal itself, which knows the checkoutId Peach's own callback fired for. See
|
|
122
132
|
// isPaymentSettled for the one refusal that changes what the caller does next.
|
|
123
133
|
abandonPayment(checkoutId: string): Promise<void>;
|
|
134
|
+
|
|
135
|
+
// The SDK-failure counterpart to abandonPayment, carrying whatever reason Peach gave. Same guard and same
|
|
136
|
+
// refusals server-side - the difference is that the attempt is recorded as failed with its code rather than
|
|
137
|
+
// simply abandoned, which for a card declined inside the checkout is the only place that code ever exists.
|
|
138
|
+
reportPaymentFailure(checkoutId: string, resultCode?: string, description?: string): Promise<void>;
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
export class ApiClient implements BookingApi {
|
|
@@ -275,10 +290,15 @@ export class ApiClient implements BookingApi {
|
|
|
275
290
|
);
|
|
276
291
|
}
|
|
277
292
|
|
|
278
|
-
async
|
|
279
|
-
return this.unwrap(
|
|
280
|
-
|
|
281
|
-
|
|
293
|
+
async getCartStatus(): Promise<CheckoutCartStatusDto> {
|
|
294
|
+
return this.unwrap(await this.client.GET('/v1/checkout/cart/status'));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async resolvePayment(): Promise<void> {
|
|
298
|
+
const result = await this.client.POST('/v1/checkout/cart/resolve-payment');
|
|
299
|
+
if (result.response.status >= 400) {
|
|
300
|
+
throw new ApiError(result.response.status, result.error);
|
|
301
|
+
}
|
|
282
302
|
}
|
|
283
303
|
|
|
284
304
|
async extendCart(): Promise<CheckoutCartDetailDto> {
|
|
@@ -287,6 +307,17 @@ export class ApiClient implements BookingApi {
|
|
|
287
307
|
);
|
|
288
308
|
}
|
|
289
309
|
|
|
310
|
+
async reportPaymentFailure(checkoutId: string, resultCode?: string, description?: string): Promise<void> {
|
|
311
|
+
const result = await this.client.POST('/v1/checkout/cart/payment-failure', {
|
|
312
|
+
body: { checkoutId, resultCode, description },
|
|
313
|
+
});
|
|
314
|
+
// Same shape as abandonPayment below, including keeping the error body: the 409s carry a code the caller
|
|
315
|
+
// branches on (see isPaymentSettled).
|
|
316
|
+
if (result.response.status >= 400) {
|
|
317
|
+
throw new ApiError(result.response.status, result.error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
290
321
|
async abandonPayment(checkoutId: string): Promise<void> {
|
|
291
322
|
const result = await this.client.POST('/v1/checkout/cart/abandon-payment', {
|
|
292
323
|
body: { checkoutId },
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
// How a BookingProvider hands the host down to the components beneath it, so a consumer writes
|
|
2
|
-
// <TicketConfigurator {productId} /> rather than restating host.api and host.cartManager on every tag. The
|
|
3
|
-
// props are still there and still win - the custom-element wrappers pass them explicitly, because each
|
|
4
|
-
// bw-* element is the root of its own Svelte tree and context does not cross that boundary.
|
|
5
|
-
|
|
6
|
-
import { getContext, setContext } from 'svelte';
|
|
7
|
-
import type { BookingHost } from './host.svelte';
|
|
8
|
-
|
|
9
|
-
const BOOKING_HOST = Symbol('booking-host');
|
|
10
|
-
|
|
11
|
-
/** Call during component initialisation - BookingProvider is the only thing that should need this. */
|
|
12
|
-
export function setBookingHostContext(host: BookingHost): void {
|
|
13
|
-
setContext(BOOKING_HOST, host);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The host from the nearest BookingProvider, or undefined when there is none. */
|
|
17
|
-
export function getBookingHostContext(): BookingHost | undefined {
|
|
18
|
-
return getContext<BookingHost | undefined>(BOOKING_HOST);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Resolves a service a component cannot render without, and says plainly what to do when it is missing.
|
|
23
|
-
* Without this the failure is a `Cannot read properties of undefined` from somewhere deep in a load().
|
|
24
|
-
*/
|
|
25
|
-
export function requireBookingService<T>(value: T | undefined, component: string, prop: string): T {
|
|
26
|
-
if (value === undefined || value === null) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`<${component}> has no ${prop}. Pass ${prop}={...}, or wrap it in ` +
|
|
29
|
-
`<BookingProvider host={createBookingHost(...)}>.`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
1
|
+
// How a BookingProvider hands the host down to the components beneath it, so a consumer writes
|
|
2
|
+
// <TicketConfigurator {productId} /> rather than restating host.api and host.cartManager on every tag. The
|
|
3
|
+
// props are still there and still win - the custom-element wrappers pass them explicitly, because each
|
|
4
|
+
// bw-* element is the root of its own Svelte tree and context does not cross that boundary.
|
|
5
|
+
|
|
6
|
+
import { getContext, setContext } from 'svelte';
|
|
7
|
+
import type { BookingHost } from './host.svelte';
|
|
8
|
+
|
|
9
|
+
const BOOKING_HOST = Symbol('booking-host');
|
|
10
|
+
|
|
11
|
+
/** Call during component initialisation - BookingProvider is the only thing that should need this. */
|
|
12
|
+
export function setBookingHostContext(host: BookingHost): void {
|
|
13
|
+
setContext(BOOKING_HOST, host);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The host from the nearest BookingProvider, or undefined when there is none. */
|
|
17
|
+
export function getBookingHostContext(): BookingHost | undefined {
|
|
18
|
+
return getContext<BookingHost | undefined>(BOOKING_HOST);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves a service a component cannot render without, and says plainly what to do when it is missing.
|
|
23
|
+
* Without this the failure is a `Cannot read properties of undefined` from somewhere deep in a load().
|
|
24
|
+
*/
|
|
25
|
+
export function requireBookingService<T>(value: T | undefined, component: string, prop: string): T {
|
|
26
|
+
if (value === undefined || value === null) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`<${component}> has no ${prop}. Pass ${prop}={...}, or wrap it in ` +
|
|
29
|
+
`<BookingProvider host={createBookingHost(...)}>.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|