@ticketlayer/elements 0.4.0 → 1.0.0
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/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
- package/dist/cjs/index.cjs.js +2 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
- package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
- package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
- package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -74
- package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
- package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
- package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
- package/dist/cjs/tl-cart.cjs.entry.js +71 -103
- package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
- package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
- package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
- package/dist/cjs/tl-login.cjs.entry.js +74 -141
- package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
- package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
- package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
- package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
- package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
- package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
- package/dist/cjs/web-query-Ce73jB4k.js +16 -0
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
- package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
- package/dist/collection/components/tl-cart/tl-cart.js +82 -107
- package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
- package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
- package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
- package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
- package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
- package/dist/collection/components/tl-login/tl-login.js +83 -147
- package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
- package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
- package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
- package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
- package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
- package/dist/collection/utils/order-summary.js +15 -135
- package/dist/collection/utils/testing/deep-text.js +29 -0
- package/dist/collection/utils/testing/fixtures.js +290 -0
- package/dist/collection/utils/testing/mock-sdk.js +13 -2
- package/dist/collection/utils/web-query.js +12 -0
- package/dist/collection/utils/web-storage.js +22 -0
- package/dist/components/ticket-selector.js +1 -0
- package/dist/components/tl-buy-modal.js +1 -1
- package/dist/components/tl-buy-tickets-button.js +1 -1
- package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
- package/dist/components/tl-cart-badge.js +1 -1
- package/dist/components/tl-cart-drawer.js +1 -1
- package/dist/components/tl-cart2.js +1 -1
- package/dist/components/tl-checkout-wrapper2.js +1 -1
- package/dist/components/tl-event-card2.js +1 -1
- package/dist/components/tl-event-list.js +1 -1
- package/dist/components/tl-login2.js +1 -1
- package/dist/components/tl-my-orders.js +1 -1
- package/dist/components/tl-occurrence-selector2.js +1 -1
- package/dist/components/tl-order-confirmation.js +1 -1
- package/dist/components/tl-order-tickets2.js +1 -1
- package/dist/components/tl-promo-code.js +1 -1
- package/dist/components/tl-seat-map.js +1 -1
- package/dist/components/tl-ticket-selector2.js +1 -1
- package/dist/components/web-query.js +1 -0
- package/dist/elements.docs.json +34 -34
- package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
- package/dist/esm/ticketlayer-elements.js +3 -3
- package/dist/esm/tl-buy-modal.entry.js +3 -3
- package/dist/esm/tl-buy-tickets-button.entry.js +90 -75
- package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
- package/dist/esm/tl-cart-badge.entry.js +75 -62
- package/dist/esm/tl-cart-drawer.entry.js +52 -24
- package/dist/esm/tl-cart.entry.js +72 -104
- package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
- package/dist/esm/tl-event-card.entry.js +44 -106
- package/dist/esm/tl-event-list.entry.js +73 -36
- package/dist/esm/tl-login.entry.js +75 -142
- package/dist/esm/tl-my-orders.entry.js +53 -93
- package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
- package/dist/esm/tl-order-confirmation.entry.js +72 -108
- package/dist/esm/tl-order-tickets_2.entry.js +49 -75
- package/dist/esm/tl-promo-code.entry.js +51 -48
- package/dist/esm/tl-seat-map.entry.js +3 -3
- package/dist/esm/web-query-Bdbb2lk5.js +14 -0
- package/dist/ticketlayer-elements/index.esm.js +1 -1
- package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
- package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
- package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
- package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
- package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
- package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
- package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
- package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
- package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
- package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
- package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
- package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
- package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
- package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
- package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
- package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
- package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
- package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
- package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
- package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
- package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
- package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
- package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
- package/dist/types/components/tl-login/tl-login.d.ts +68 -22
- package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
- package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
- package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
- package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
- package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
- package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
- package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
- package/dist/types/components.d.ts +270 -62
- package/dist/types/utils/order-summary.d.ts +14 -97
- package/dist/types/utils/testing/deep-text.d.ts +13 -0
- package/dist/types/utils/testing/fixtures.d.ts +225 -0
- package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
- package/dist/types/utils/web-query.d.ts +18 -0
- package/dist/types/utils/web-storage.d.ts +16 -0
- package/elements.catalogue.json +602 -50
- package/package.json +8 -6
- package/dist/cjs/money-BtmM2gt-.js +0 -118
- package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
- package/dist/components/money.js +0 -1
- package/dist/components/order-summary.js +0 -1
- package/dist/esm/money-4ig0-WB6.js +0 -112
- package/dist/esm/order-summary-DiDtvfZM.js +0 -136
- package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
- package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
- package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
- package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
- package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
- package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
- package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
- package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
- package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
- package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
- package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
- package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
- package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
- package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
|
@@ -22,6 +22,9 @@ export class TlTicketSelector {
|
|
|
22
22
|
this.buttonLabel = 'Add to Cart';
|
|
23
23
|
this.element = createElement(ticketSelectorCore, {
|
|
24
24
|
client: () => getSDK(),
|
|
25
|
+
// What "the client is up" means on the web. The core waits on it in its
|
|
26
|
+
// own mount; the harness no longer decides when to load (TKT-197).
|
|
27
|
+
clientReady: () => whenSDK(),
|
|
25
28
|
emit: (event, detail) => this.forward(event, detail),
|
|
26
29
|
});
|
|
27
30
|
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
@@ -39,9 +42,14 @@ export class TlTicketSelector {
|
|
|
39
42
|
set feeQuote(quote) {
|
|
40
43
|
this.element.actions.setFeeQuote(quote);
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A new event or occurrence is a new subject: the element is torn down and
|
|
47
|
+
* mounted again, so whatever the core set up for the old one is undone
|
|
48
|
+
* before anything is set up for the new one.
|
|
49
|
+
*/
|
|
42
50
|
watchPropsChange() {
|
|
43
51
|
this.syncProps();
|
|
44
|
-
this.
|
|
52
|
+
this.remount();
|
|
45
53
|
}
|
|
46
54
|
syncProps() {
|
|
47
55
|
this.element.setProps({
|
|
@@ -51,32 +59,38 @@ export class TlTicketSelector {
|
|
|
51
59
|
buttonLabel: this.buttonLabel,
|
|
52
60
|
});
|
|
53
61
|
}
|
|
54
|
-
|
|
62
|
+
/**
|
|
63
|
+
* On screen: render what the core says and put the core to work. What "to
|
|
64
|
+
* work" means is the core's own business - for this element, waiting for the
|
|
65
|
+
* client and reading the ticket types.
|
|
66
|
+
*
|
|
67
|
+
* `connectedCallback` and not `componentWillLoad` because this runs again
|
|
68
|
+
* when the element is moved in the DOM, and that is exactly when a mount and
|
|
69
|
+
* its teardown have to pair up.
|
|
70
|
+
*/
|
|
71
|
+
connectedCallback() {
|
|
55
72
|
this.syncProps();
|
|
56
|
-
this.unsubscribe = this.element.subscribe((view) => {
|
|
73
|
+
this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
|
|
57
74
|
this.view = view;
|
|
58
|
-
});
|
|
75
|
+
}));
|
|
59
76
|
this.view = this.element.view;
|
|
60
|
-
|
|
77
|
+
this.teardown = this.element.mount();
|
|
61
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Off screen: run the mount's teardown, and nothing more. `destroy()` is
|
|
81
|
+
* terminal and Stencil calls this for a move as well as a removal, so
|
|
82
|
+
* destroying here would leave a re-inserted element frozen. The instance
|
|
83
|
+
* holds no listener and no subscription once these two have run.
|
|
84
|
+
*/
|
|
62
85
|
disconnectedCallback() {
|
|
86
|
+
this.teardown?.();
|
|
87
|
+
this.teardown = undefined;
|
|
63
88
|
this.unsubscribe?.();
|
|
64
|
-
this.
|
|
89
|
+
this.unsubscribe = undefined;
|
|
65
90
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
async waitForSDKAndLoad() {
|
|
70
|
-
if (!this.eventId || !this.occurrenceId) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
await whenSDK();
|
|
75
|
-
await this.element.actions.load();
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
|
|
79
|
-
}
|
|
91
|
+
remount() {
|
|
92
|
+
this.teardown?.();
|
|
93
|
+
this.teardown = this.element.mount();
|
|
80
94
|
}
|
|
81
95
|
forward(event, detail) {
|
|
82
96
|
if (event === 'tlAddToCart')
|
|
@@ -1,137 +1,17 @@
|
|
|
1
|
-
import { expressesFee, minorUnits, FEE_LABEL_FALLBACK } from "./cart-totals";
|
|
2
|
-
/** Money arrives as minor-unit integers or decimal strings ("50.00"). */
|
|
3
|
-
export function formatPrice(value, currency = 'GBP') {
|
|
4
|
-
if (value === null || value === undefined || value === '')
|
|
5
|
-
return '';
|
|
6
|
-
const fmt = new Intl.NumberFormat('en-GB', { style: 'currency', currency });
|
|
7
|
-
if (typeof value === 'number')
|
|
8
|
-
return fmt.format(value / 100);
|
|
9
|
-
const n = Number(value);
|
|
10
|
-
return Number.isNaN(n) ? String(value) : fmt.format(n);
|
|
11
|
-
}
|
|
12
|
-
export function formatDateTime(iso) {
|
|
13
|
-
return new Date(iso).toLocaleDateString('en-GB', {
|
|
14
|
-
weekday: 'short',
|
|
15
|
-
day: 'numeric',
|
|
16
|
-
month: 'short',
|
|
17
|
-
year: 'numeric',
|
|
18
|
-
hour: '2-digit',
|
|
19
|
-
minute: '2-digit',
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
export function formatDate(iso) {
|
|
23
|
-
return new Date(iso).toLocaleDateString('en-GB', {
|
|
24
|
-
day: 'numeric',
|
|
25
|
-
month: 'short',
|
|
26
|
-
year: 'numeric',
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
/** Line items that are tickets (fees, taxes and discounts have their own type). */
|
|
30
|
-
export function ticketItems(order) {
|
|
31
|
-
return (order.items ?? []).filter((i) => !i.type || i.type === 'ticket');
|
|
32
|
-
}
|
|
33
|
-
export function ticketCount(order) {
|
|
34
|
-
const fromItems = ticketItems(order).reduce((sum, i) => sum + (Number(i.quantity) || 0), 0);
|
|
35
|
-
return fromItems || order.tickets?.length || 0;
|
|
36
|
-
}
|
|
37
|
-
/** Event name as the order view exposes it: the first ticket line's name. */
|
|
38
|
-
export function orderEventName(order) {
|
|
39
|
-
return order.eventName || ticketItems(order)[0]?.name || order.items?.[0]?.name || null;
|
|
40
|
-
}
|
|
41
|
-
export function orderCustomerEmail(order, customer) {
|
|
42
|
-
const fromOrder = order?.customerEmail || order?.customer?.email;
|
|
43
|
-
if (fromOrder)
|
|
44
|
-
return fromOrder;
|
|
45
|
-
const c = customer;
|
|
46
|
-
return c?.email || null;
|
|
47
|
-
}
|
|
48
|
-
export function statusLabel(status) {
|
|
49
|
-
if (!status)
|
|
50
|
-
return '';
|
|
51
|
-
return status.replace(/_/g, ' ').replace(/^\w/, (m) => m.toUpperCase());
|
|
52
|
-
}
|
|
53
|
-
export function statusTone(status) {
|
|
54
|
-
switch (status) {
|
|
55
|
-
case 'confirmed':
|
|
56
|
-
return 'success';
|
|
57
|
-
case 'completed':
|
|
58
|
-
case 'fulfilled':
|
|
59
|
-
return 'info';
|
|
60
|
-
case 'pending':
|
|
61
|
-
case 'processing':
|
|
62
|
-
return 'warning';
|
|
63
|
-
case 'cancelled':
|
|
64
|
-
case 'expired':
|
|
65
|
-
case 'failed':
|
|
66
|
-
return 'error';
|
|
67
|
-
default:
|
|
68
|
-
return 'muted';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export function errorMessage(err, fallback) {
|
|
72
|
-
return err instanceof Error && err.message ? err.message : fallback;
|
|
73
|
-
}
|
|
74
|
-
/** True for the "not signed in" family of API errors (401/403, NO_SESSION, NOT_AUTHENTICATED). */
|
|
75
|
-
export function isAuthError(err) {
|
|
76
|
-
const e = err;
|
|
77
|
-
if (!e || typeof e !== 'object')
|
|
78
|
-
return false;
|
|
79
|
-
return e.status === 401 || e.status === 403 || e.code === 'NO_SESSION' || e.code === 'NOT_AUTHENTICATED';
|
|
80
|
-
}
|
|
81
1
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
2
|
+
* The order helpers, as the components have always imported them.
|
|
3
|
+
*
|
|
4
|
+
* Everything here now lives in `@ticketlayer/elements-core`
|
|
5
|
+
* (`src/orders.ts`): the order shape, the money rows, the status tone, the
|
|
6
|
+
* date and price formatting and the occurrence lookup all moved across with
|
|
7
|
+
* the split of `tl-my-orders` and `tl-order-confirmation` (TKT-69), so a
|
|
8
|
+
* native harness answers the same questions the same way. This module stays as
|
|
9
|
+
* the import path, unchanged for callers, because `tl-order-tickets` and
|
|
10
|
+
* `tl-ticket` are not split yet and read these helpers from here.
|
|
11
|
+
*
|
|
12
|
+
* `resolveOccurrence` is the one signature that moved: it takes the core's
|
|
13
|
+
* `ElementClient` rather than this package's `TicketlayerSDK`. A
|
|
14
|
+
* `TicketlayerSDK` satisfies that shape, so every existing call still
|
|
15
|
+
* compiles, which is why it is re-exported rather than wrapped.
|
|
85
16
|
*/
|
|
86
|
-
export
|
|
87
|
-
const events = sdk.getEventsManager();
|
|
88
|
-
const listing = await events.list({ limit: maxEvents });
|
|
89
|
-
const candidates = listing.items.slice(0, maxEvents);
|
|
90
|
-
for (const candidate of candidates) {
|
|
91
|
-
const detail = (await events.get(candidate.id).catch(() => null));
|
|
92
|
-
const occ = detail?.occurrences?.find((o) => o.id === occurrenceId);
|
|
93
|
-
if (occ) {
|
|
94
|
-
return {
|
|
95
|
-
eventId: detail?.id ?? candidate.id,
|
|
96
|
-
eventName: detail?.name ?? candidate.name ?? '',
|
|
97
|
-
startsAt: occ.startsAt ?? null,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
export function orderTotals(order) {
|
|
104
|
-
const o = order || {};
|
|
105
|
-
const items = Array.isArray(o.items) ? o.items : [];
|
|
106
|
-
const feeLines = items.filter((i) => i.type === 'fee');
|
|
107
|
-
const discountLines = items.filter((i) => i.type === 'discount');
|
|
108
|
-
const tickets = ticketItems(o);
|
|
109
|
-
const feeFromLines = feeLines.length
|
|
110
|
-
? feeLines.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
|
|
111
|
-
: null;
|
|
112
|
-
const fees = feeFromLines ?? minorUnits(o.totalFees) ?? 0;
|
|
113
|
-
const declaredSubtotal = minorUnits(o.subtotal);
|
|
114
|
-
const subtotal = tickets.length
|
|
115
|
-
? tickets.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
|
|
116
|
-
: (declaredSubtotal ?? 0) - fees;
|
|
117
|
-
const discountFromLines = discountLines.length
|
|
118
|
-
? discountLines.reduce((sum, i) => sum + Math.abs(minorUnits(i.subtotal) ?? 0), 0)
|
|
119
|
-
: null;
|
|
120
|
-
const discount = minorUnits(o.totalDiscounts) ?? discountFromLines ?? 0;
|
|
121
|
-
const tax = minorUnits(o.totalTax) ?? 0;
|
|
122
|
-
const named = feeLines.some((i) => typeof i.name === 'string' && !!i.name);
|
|
123
|
-
return {
|
|
124
|
-
currency: o.currency || 'GBP',
|
|
125
|
-
subtotal,
|
|
126
|
-
fees,
|
|
127
|
-
feeLabel: feeLines.map((i) => i.name).find((n) => !!n) || FEE_LABEL_FALLBACK,
|
|
128
|
-
showFee: expressesFee(feeFromLines ?? minorUnits(o.totalFees), named),
|
|
129
|
-
tax,
|
|
130
|
-
discount,
|
|
131
|
-
total: minorUnits(o.total) ?? subtotal + fees + tax - discount,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/** The rows add up to the total the order reports. Asserted in the specs. */
|
|
135
|
-
export function totalsAgree(totals) {
|
|
136
|
-
return totals.subtotal + totals.fees + totals.tax - totals.discount === totals.total;
|
|
137
|
-
}
|
|
17
|
+
export { errorMessage, formatDate, formatDateTime, formatPrice, isAuthError, orderCustomerEmail, orderEventName, orderTotals, resolveOccurrence, statusLabel, statusTone, ticketCount, ticketItems, totalsAgree, } from '@ticketlayer/elements-core';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text an element actually shows, shadow roots included.
|
|
3
|
+
*
|
|
4
|
+
* `textContent` stops at a shadow boundary, and every element here renders
|
|
5
|
+
* into one (and several compose others: `tl-cart-drawer` holds a `tl-cart`,
|
|
6
|
+
* `tl-order-tickets` a grid of `tl-ticket`). A check that an element rendered
|
|
7
|
+
* something has to see through that, or it reads an empty string from an
|
|
8
|
+
* element that is perfectly full.
|
|
9
|
+
*
|
|
10
|
+
* Used by the stories' `expectRendered` in the browser and by
|
|
11
|
+
* story-coverage.spec.tsx in jsdom, so both are asking the same question.
|
|
12
|
+
*/
|
|
13
|
+
export function deepText(node) {
|
|
14
|
+
if (!node)
|
|
15
|
+
return '';
|
|
16
|
+
const parts = [];
|
|
17
|
+
const walk = (current) => {
|
|
18
|
+
if (current.nodeType === 3) {
|
|
19
|
+
parts.push(current.nodeValue ?? '');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const shadow = current.shadowRoot;
|
|
23
|
+
if (shadow)
|
|
24
|
+
walk(shadow);
|
|
25
|
+
current.childNodes.forEach(walk);
|
|
26
|
+
};
|
|
27
|
+
walk(node);
|
|
28
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
29
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export const FIXTURE_CUSTOMER = {
|
|
2
|
+
id: 'cus_ada',
|
|
3
|
+
email: 'ada@example.com',
|
|
4
|
+
firstName: 'Ada',
|
|
5
|
+
lastName: 'Lovelace',
|
|
6
|
+
};
|
|
7
|
+
/** `events.list` items, i.e. what a card is handed by `tl-event-list`. */
|
|
8
|
+
export const FIXTURE_EVENT_CARDS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'evt_midnight',
|
|
11
|
+
name: 'Midnight Quartet',
|
|
12
|
+
shortDescription: 'Late night strings in the Old Hall',
|
|
13
|
+
availabilityStatus: 'available',
|
|
14
|
+
venue: { id: 'ven_old_hall', name: 'The Old Hall', city: 'Bristol' },
|
|
15
|
+
nextOccurrence: { startsAt: '2030-06-14T19:30:00Z' },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'evt_gala',
|
|
19
|
+
name: 'Summer Gala',
|
|
20
|
+
shortDescription: 'The season closes with the full orchestra',
|
|
21
|
+
availabilityStatus: 'limited',
|
|
22
|
+
venue: { id: 'ven_pavilion', name: 'Riverside Pavilion', city: 'Bath' },
|
|
23
|
+
nextOccurrence: { startsAt: '2030-07-05T18:00:00Z' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'evt_lates',
|
|
27
|
+
name: 'Gallery Lates',
|
|
28
|
+
shortDescription: 'Music after hours among the paintings',
|
|
29
|
+
availabilityStatus: 'sold_out',
|
|
30
|
+
venue: { id: 'ven_gallery', name: 'City Gallery', city: 'Bristol' },
|
|
31
|
+
nextOccurrence: { startsAt: '2030-08-02T20:00:00Z' },
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
/** The occurrences of `evt_midnight`: enough dates for the calendar view. */
|
|
35
|
+
const MIDNIGHT_OCCURRENCES = [
|
|
36
|
+
{
|
|
37
|
+
id: 'evoc_1',
|
|
38
|
+
startsAt: '2030-06-14T19:30:00Z',
|
|
39
|
+
endsAt: '2030-06-14T21:30:00Z',
|
|
40
|
+
availabilityStatus: 'available',
|
|
41
|
+
availableCapacity: 120,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'evoc_2',
|
|
45
|
+
startsAt: '2030-06-15T19:30:00Z',
|
|
46
|
+
endsAt: '2030-06-15T21:30:00Z',
|
|
47
|
+
availabilityStatus: 'available',
|
|
48
|
+
availableCapacity: 40,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'evoc_3',
|
|
52
|
+
startsAt: '2030-06-21T19:30:00Z',
|
|
53
|
+
endsAt: '2030-06-21T21:30:00Z',
|
|
54
|
+
availabilityStatus: 'sold_out',
|
|
55
|
+
availableCapacity: 0,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
/** `events.get` details, keyed by event id. */
|
|
59
|
+
export const FIXTURE_EVENT_DETAILS = {
|
|
60
|
+
evt_midnight: {
|
|
61
|
+
id: 'evt_midnight',
|
|
62
|
+
name: 'Midnight Quartet',
|
|
63
|
+
status: 'on_sale',
|
|
64
|
+
shortDescription: 'Late night strings in the Old Hall',
|
|
65
|
+
venueId: 'ven_old_hall',
|
|
66
|
+
venueName: 'The Old Hall',
|
|
67
|
+
venueCity: 'Bristol',
|
|
68
|
+
occurrences: MIDNIGHT_OCCURRENCES,
|
|
69
|
+
},
|
|
70
|
+
evt_gala: {
|
|
71
|
+
id: 'evt_gala',
|
|
72
|
+
name: 'Summer Gala',
|
|
73
|
+
status: 'on_sale',
|
|
74
|
+
shortDescription: 'The season closes with the full orchestra',
|
|
75
|
+
venueId: 'ven_pavilion',
|
|
76
|
+
venueName: 'Riverside Pavilion',
|
|
77
|
+
venueCity: 'Bath',
|
|
78
|
+
occurrences: [
|
|
79
|
+
{
|
|
80
|
+
id: 'evoc_gala',
|
|
81
|
+
startsAt: '2030-07-05T18:00:00Z',
|
|
82
|
+
endsAt: '2030-07-05T22:00:00Z',
|
|
83
|
+
availabilityStatus: 'available',
|
|
84
|
+
availableCapacity: 300,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/** `events.getTicketTypes` for `evoc_1`. */
|
|
90
|
+
export const FIXTURE_TICKET_TYPES = [
|
|
91
|
+
{
|
|
92
|
+
id: 'tkt_adult',
|
|
93
|
+
occurrenceId: 'evoc_1',
|
|
94
|
+
name: 'Adult',
|
|
95
|
+
description: 'Standing, unreserved',
|
|
96
|
+
price: 2500,
|
|
97
|
+
currency: 'GBP',
|
|
98
|
+
availableQuantity: 120,
|
|
99
|
+
maxPerOrder: 6,
|
|
100
|
+
minPerOrder: 0,
|
|
101
|
+
availabilityStatus: 'available',
|
|
102
|
+
sortOrder: 1,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'tkt_concession',
|
|
106
|
+
occurrenceId: 'evoc_1',
|
|
107
|
+
name: 'Concession',
|
|
108
|
+
description: 'Students and over 65s',
|
|
109
|
+
price: 1500,
|
|
110
|
+
currency: 'GBP',
|
|
111
|
+
availableQuantity: 20,
|
|
112
|
+
maxPerOrder: 4,
|
|
113
|
+
minPerOrder: 0,
|
|
114
|
+
availabilityStatus: 'limited',
|
|
115
|
+
sortOrder: 2,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'tkt_gallery',
|
|
119
|
+
occurrenceId: 'evoc_1',
|
|
120
|
+
name: 'Gallery',
|
|
121
|
+
description: 'Seated, upper level',
|
|
122
|
+
price: 4000,
|
|
123
|
+
currency: 'GBP',
|
|
124
|
+
availableQuantity: 0,
|
|
125
|
+
maxPerOrder: 4,
|
|
126
|
+
minPerOrder: 0,
|
|
127
|
+
availabilityStatus: 'sold_out',
|
|
128
|
+
sortOrder: 3,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
/** A cart with lines, a fee the surface quoted and a discount. */
|
|
132
|
+
export const FIXTURE_CART = {
|
|
133
|
+
items: [
|
|
134
|
+
{
|
|
135
|
+
id: 'ci_adult',
|
|
136
|
+
ticketTypeId: 'tkt_adult',
|
|
137
|
+
ticketTypeName: 'Adult',
|
|
138
|
+
eventName: 'Midnight Quartet',
|
|
139
|
+
occurrenceDate: '2030-06-14T19:30:00Z',
|
|
140
|
+
quantity: 2,
|
|
141
|
+
unitPrice: 2500,
|
|
142
|
+
totalPrice: 5000,
|
|
143
|
+
subtotal: 5000,
|
|
144
|
+
currency: 'GBP',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'ci_concession',
|
|
148
|
+
ticketTypeId: 'tkt_concession',
|
|
149
|
+
ticketTypeName: 'Concession',
|
|
150
|
+
eventName: 'Midnight Quartet',
|
|
151
|
+
occurrenceDate: '2030-06-14T19:30:00Z',
|
|
152
|
+
quantity: 1,
|
|
153
|
+
unitPrice: 1500,
|
|
154
|
+
totalPrice: 1500,
|
|
155
|
+
subtotal: 1500,
|
|
156
|
+
currency: 'GBP',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
itemCount: 3,
|
|
160
|
+
subtotal: 6500,
|
|
161
|
+
fees: 325,
|
|
162
|
+
discount: 500,
|
|
163
|
+
total: 6325,
|
|
164
|
+
hasFees: true,
|
|
165
|
+
feeLabel: 'Booking fee',
|
|
166
|
+
currency: 'GBP',
|
|
167
|
+
};
|
|
168
|
+
/** An empty cart, for the states an element only reaches with nothing in it. */
|
|
169
|
+
export const FIXTURE_EMPTY_CART = {
|
|
170
|
+
items: [],
|
|
171
|
+
itemCount: 0,
|
|
172
|
+
subtotal: 0,
|
|
173
|
+
fees: 0,
|
|
174
|
+
discount: 0,
|
|
175
|
+
total: 0,
|
|
176
|
+
hasFees: false,
|
|
177
|
+
feeLabel: null,
|
|
178
|
+
currency: 'GBP',
|
|
179
|
+
};
|
|
180
|
+
/** Passes on the order: what `tl-ticket` renders and `tl-order-tickets` grids. */
|
|
181
|
+
export const FIXTURE_TICKETS = [
|
|
182
|
+
{
|
|
183
|
+
id: 'pas_adult_1',
|
|
184
|
+
barcode: 'TLQ-3H7K-9F2D-0001',
|
|
185
|
+
type: 'ticket',
|
|
186
|
+
status: 'active',
|
|
187
|
+
holderName: 'Ada Lovelace',
|
|
188
|
+
name: 'Adult - Midnight Quartet',
|
|
189
|
+
admissionStatus: 'not_redeemed',
|
|
190
|
+
eventOccurrenceId: 'evoc_1',
|
|
191
|
+
seatLabel: 'Stalls C12',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'pas_adult_2',
|
|
195
|
+
barcode: 'TLQ-3H7K-9F2D-0002',
|
|
196
|
+
type: 'ticket',
|
|
197
|
+
status: 'active',
|
|
198
|
+
holderName: 'Ada Lovelace',
|
|
199
|
+
name: 'Adult - Midnight Quartet',
|
|
200
|
+
admissionStatus: 'fully_redeemed',
|
|
201
|
+
eventOccurrenceId: 'evoc_1',
|
|
202
|
+
seatLabel: 'Stalls C13',
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
export const FIXTURE_ORDER = {
|
|
206
|
+
id: 'ord_midnight',
|
|
207
|
+
orderNumber: 'ORD-MQ7841',
|
|
208
|
+
status: 'confirmed',
|
|
209
|
+
paymentStatus: 'paid',
|
|
210
|
+
currency: 'GBP',
|
|
211
|
+
items: [
|
|
212
|
+
{
|
|
213
|
+
name: 'Midnight Quartet - Adult',
|
|
214
|
+
description: null,
|
|
215
|
+
type: 'ticket',
|
|
216
|
+
quantity: 2,
|
|
217
|
+
unitPrice: 2500,
|
|
218
|
+
subtotal: 5000,
|
|
219
|
+
},
|
|
220
|
+
{ name: 'Booking fee', description: null, type: 'fee', quantity: 1, unitPrice: 250, subtotal: 250 },
|
|
221
|
+
],
|
|
222
|
+
subtotal: 5000,
|
|
223
|
+
totalFees: 250,
|
|
224
|
+
totalTax: 0,
|
|
225
|
+
total: 5250,
|
|
226
|
+
tickets: FIXTURE_TICKETS,
|
|
227
|
+
expiresAt: null,
|
|
228
|
+
createdAt: '2030-05-02T10:15:00Z',
|
|
229
|
+
};
|
|
230
|
+
const OLDER_ORDER = {
|
|
231
|
+
id: 'ord_gala',
|
|
232
|
+
orderNumber: 'ORD-SG1290',
|
|
233
|
+
status: 'confirmed',
|
|
234
|
+
paymentStatus: 'paid',
|
|
235
|
+
currency: 'GBP',
|
|
236
|
+
items: [
|
|
237
|
+
{
|
|
238
|
+
name: 'Summer Gala - Concession',
|
|
239
|
+
description: null,
|
|
240
|
+
type: 'ticket',
|
|
241
|
+
quantity: 1,
|
|
242
|
+
unitPrice: 1500,
|
|
243
|
+
subtotal: 1500,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
subtotal: 1500,
|
|
247
|
+
totalFees: 0,
|
|
248
|
+
totalTax: 0,
|
|
249
|
+
total: 1500,
|
|
250
|
+
tickets: [],
|
|
251
|
+
expiresAt: null,
|
|
252
|
+
createdAt: '2030-04-11T09:00:00Z',
|
|
253
|
+
};
|
|
254
|
+
export const FIXTURE_ORDERS = [FIXTURE_ORDER, OLDER_ORDER];
|
|
255
|
+
/** The managers every dataset shares: the channel's catalogue and its orders. */
|
|
256
|
+
const catalogue = {
|
|
257
|
+
events: {
|
|
258
|
+
list: async () => ({ items: FIXTURE_EVENT_CARDS }),
|
|
259
|
+
get: async (id) => FIXTURE_EVENT_DETAILS[id] ?? FIXTURE_EVENT_DETAILS.evt_midnight,
|
|
260
|
+
getTicketTypes: async () => FIXTURE_TICKET_TYPES,
|
|
261
|
+
},
|
|
262
|
+
orders: {
|
|
263
|
+
mine: async () => FIXTURE_ORDERS,
|
|
264
|
+
get: async (orderId) => FIXTURE_ORDERS.find((o) => o.id === orderId) ?? FIXTURE_ORDER,
|
|
265
|
+
ticketPdf: async () => new Blob(['%PDF-1.4 fixture'], { type: 'application/pdf' }),
|
|
266
|
+
},
|
|
267
|
+
presale: {
|
|
268
|
+
redeem: async () => ({ benefit: 'Presale unlocked', alreadyHeld: false }),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* The datasets a story may ask for by name, through
|
|
273
|
+
* `parameters.ticketlayer.dataset`. Keep the set small: a dataset is a state
|
|
274
|
+
* of the whole channel, not a per-element fixture.
|
|
275
|
+
*/
|
|
276
|
+
export const FIXTURE_DATASETS = {
|
|
277
|
+
/** Signed in, three events on sale, a cart with lines and two orders. */
|
|
278
|
+
default: {
|
|
279
|
+
...catalogue,
|
|
280
|
+
cart: { get: async () => FIXTURE_CART },
|
|
281
|
+
authState: { customer: FIXTURE_CUSTOMER, authenticated: true },
|
|
282
|
+
},
|
|
283
|
+
/** Nobody signed in and nothing in the cart. */
|
|
284
|
+
'signed-out': {
|
|
285
|
+
...catalogue,
|
|
286
|
+
cart: { get: async () => FIXTURE_EMPTY_CART },
|
|
287
|
+
authState: { customer: null, authenticated: false },
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
export const FIXTURE_DATASET_NAMES = Object.keys(FIXTURE_DATASETS);
|
|
@@ -58,17 +58,28 @@ export function createMockSDK(opts = {}) {
|
|
|
58
58
|
redeem: async () => ({ benefit: 'presale.test', alreadyHeld: false }),
|
|
59
59
|
...opts.presale,
|
|
60
60
|
};
|
|
61
|
+
const modalCalls = [];
|
|
61
62
|
const sdk = {
|
|
62
63
|
getEventsManager: () => eventsManager,
|
|
63
64
|
getCartManager: () => cartManager,
|
|
64
65
|
getOrdersManager: () => ordersManager,
|
|
65
66
|
on,
|
|
66
67
|
off,
|
|
67
|
-
event: () => ({
|
|
68
|
-
|
|
68
|
+
event: (eventId) => ({
|
|
69
|
+
openModal: (options = {}) => {
|
|
70
|
+
modalCalls.push({ kind: 'event', eventId, options });
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
checkout: {
|
|
74
|
+
openModal: (options = {}) => {
|
|
75
|
+
modalCalls.push({ kind: 'checkout', options });
|
|
76
|
+
},
|
|
77
|
+
closeModal: () => undefined,
|
|
78
|
+
},
|
|
69
79
|
closeModal: () => undefined,
|
|
70
80
|
emit,
|
|
71
81
|
authState,
|
|
82
|
+
modalCalls,
|
|
72
83
|
};
|
|
73
84
|
if (!opts.withoutAuth)
|
|
74
85
|
sdk.getAuthManager = () => authManager;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const webStorage = {
|
|
2
|
+
get(key) {
|
|
3
|
+
if (typeof window === 'undefined')
|
|
4
|
+
return null;
|
|
5
|
+
try {
|
|
6
|
+
return window.localStorage.getItem(key);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
set(key, value) {
|
|
13
|
+
if (typeof window === 'undefined')
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
window.localStorage.setItem(key, value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// localStorage may be unavailable.
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|