@ticketlayer/elements-core 0.1.0 → 0.2.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/README.md +141 -4
- package/dist/cjs/cores/buy-tickets-button.js +213 -0
- package/dist/cjs/cores/buy-tickets-wrapper.js +106 -0
- package/dist/cjs/cores/cart-badge.js +150 -0
- package/dist/cjs/cores/cart-drawer.js +122 -0
- package/dist/cjs/cores/cart.js +247 -0
- package/dist/cjs/cores/checkout-wrapper.js +75 -0
- package/dist/cjs/cores/event-card.js +209 -0
- package/dist/cjs/cores/event-list.js +109 -0
- package/dist/cjs/cores/index.js +64 -1
- package/dist/cjs/cores/login.js +309 -0
- package/dist/cjs/cores/my-orders.js +221 -0
- package/dist/cjs/cores/occurrence-selector.js +528 -0
- package/dist/cjs/cores/order-confirmation.js +301 -0
- package/dist/cjs/cores/order-tickets.js +172 -0
- package/dist/cjs/cores/promo-code.js +136 -0
- package/dist/cjs/cores/ticket-selector.js +30 -0
- package/dist/cjs/define.js +76 -0
- package/dist/cjs/index.js +17 -1
- package/dist/cjs/manifest.js +5 -0
- package/dist/cjs/orders.js +169 -0
- package/dist/esm/client.d.ts +106 -0
- package/dist/esm/cores/buy-tickets-button.d.ts +55 -0
- package/dist/esm/cores/buy-tickets-button.js +210 -0
- package/dist/esm/cores/buy-tickets-wrapper.d.ts +27 -0
- package/dist/esm/cores/buy-tickets-wrapper.js +103 -0
- package/dist/esm/cores/cart-badge.d.ts +48 -0
- package/dist/esm/cores/cart-badge.js +147 -0
- package/dist/esm/cores/cart-drawer.d.ts +44 -0
- package/dist/esm/cores/cart-drawer.js +119 -0
- package/dist/esm/cores/cart.d.ts +105 -0
- package/dist/esm/cores/cart.js +244 -0
- package/dist/esm/cores/checkout-wrapper.d.ts +22 -0
- package/dist/esm/cores/checkout-wrapper.js +72 -0
- package/dist/esm/cores/event-card.d.ts +80 -0
- package/dist/esm/cores/event-card.js +205 -0
- package/dist/esm/cores/event-list.d.ts +35 -0
- package/dist/esm/cores/event-list.js +106 -0
- package/dist/esm/cores/index.d.ts +28 -0
- package/dist/esm/cores/index.js +42 -0
- package/dist/esm/cores/login.d.ts +125 -0
- package/dist/esm/cores/login.js +306 -0
- package/dist/esm/cores/my-orders.d.ts +63 -0
- package/dist/esm/cores/my-orders.js +218 -0
- package/dist/esm/cores/occurrence-selector.d.ts +170 -0
- package/dist/esm/cores/occurrence-selector.js +525 -0
- package/dist/esm/cores/order-confirmation.d.ts +99 -0
- package/dist/esm/cores/order-confirmation.js +298 -0
- package/dist/esm/cores/order-tickets.d.ts +46 -0
- package/dist/esm/cores/order-tickets.js +169 -0
- package/dist/esm/cores/promo-code.d.ts +56 -0
- package/dist/esm/cores/promo-code.js +132 -0
- package/dist/esm/cores/ticket-selector.js +30 -0
- package/dist/esm/define.d.ts +146 -0
- package/dist/esm/define.js +75 -0
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.js +2 -1
- package/dist/esm/manifest.d.ts +12 -0
- package/dist/esm/manifest.js +5 -0
- package/dist/esm/orders.d.ts +110 -0
- package/dist/esm/orders.js +154 -0
- package/package.json +2 -2
package/dist/cjs/define.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_QUERY = exports.NO_STORAGE = void 0;
|
|
3
4
|
exports.defineElement = defineElement;
|
|
4
5
|
exports.createElement = createElement;
|
|
6
|
+
/** The store a core gets when its harness supplies none: it forgets. */
|
|
7
|
+
exports.NO_STORAGE = {
|
|
8
|
+
get: () => null,
|
|
9
|
+
set: () => undefined,
|
|
10
|
+
};
|
|
11
|
+
/** The parameters a core gets when its harness supplies none: there are none. */
|
|
12
|
+
exports.NO_QUERY = {
|
|
13
|
+
get: () => null,
|
|
14
|
+
};
|
|
5
15
|
const CATEGORIES = [
|
|
6
16
|
'discovery',
|
|
7
17
|
'purchase',
|
|
@@ -71,6 +81,8 @@ function createElement(core, options = {}) {
|
|
|
71
81
|
style: 'currency',
|
|
72
82
|
currency: currency || fallbackCurrency,
|
|
73
83
|
}).format(amountInMinorUnits / 100);
|
|
84
|
+
const storage = options.storage ?? exports.NO_STORAGE;
|
|
85
|
+
const query = options.query ?? exports.NO_QUERY;
|
|
74
86
|
const viewContext = () => ({ props, state, strings, formatMoney });
|
|
75
87
|
const buildView = () => {
|
|
76
88
|
const ctx = viewContext();
|
|
@@ -89,6 +101,15 @@ function createElement(core, options = {}) {
|
|
|
89
101
|
for (const listener of [...listeners])
|
|
90
102
|
listener(view);
|
|
91
103
|
};
|
|
104
|
+
const whenClient = async () => {
|
|
105
|
+
if (options.clientReady)
|
|
106
|
+
await options.clientReady();
|
|
107
|
+
const client = resolveClient();
|
|
108
|
+
if (!client) {
|
|
109
|
+
throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
|
|
110
|
+
}
|
|
111
|
+
return client;
|
|
112
|
+
};
|
|
92
113
|
const actionContext = {
|
|
93
114
|
get props() {
|
|
94
115
|
return props;
|
|
@@ -126,6 +147,9 @@ function createElement(core, options = {}) {
|
|
|
126
147
|
return null;
|
|
127
148
|
}
|
|
128
149
|
},
|
|
150
|
+
storage,
|
|
151
|
+
query,
|
|
152
|
+
whenClient,
|
|
129
153
|
after(ms, run) {
|
|
130
154
|
if (destroyed)
|
|
131
155
|
return;
|
|
@@ -138,6 +162,52 @@ function createElement(core, options = {}) {
|
|
|
138
162
|
},
|
|
139
163
|
};
|
|
140
164
|
const actions = core.actions(actionContext);
|
|
165
|
+
// The live mount, as an identity rather than a boolean: a teardown closes
|
|
166
|
+
// over the token it was made for, so a stale one tears nothing down.
|
|
167
|
+
let mountToken = null;
|
|
168
|
+
let cleanups = [];
|
|
169
|
+
const teardownMount = (token) => {
|
|
170
|
+
if (mountToken !== token)
|
|
171
|
+
return;
|
|
172
|
+
mountToken = null;
|
|
173
|
+
const pending = cleanups;
|
|
174
|
+
cleanups = [];
|
|
175
|
+
for (const cleanup of pending.reverse())
|
|
176
|
+
cleanup();
|
|
177
|
+
};
|
|
178
|
+
const mount = () => {
|
|
179
|
+
if (destroyed)
|
|
180
|
+
return () => { };
|
|
181
|
+
if (mountToken)
|
|
182
|
+
teardownMount(mountToken);
|
|
183
|
+
const token = {};
|
|
184
|
+
mountToken = token;
|
|
185
|
+
const live = () => mountToken === token && !destroyed;
|
|
186
|
+
const returned = core.mount?.({
|
|
187
|
+
...actionContext,
|
|
188
|
+
get props() {
|
|
189
|
+
return props;
|
|
190
|
+
},
|
|
191
|
+
get state() {
|
|
192
|
+
return state;
|
|
193
|
+
},
|
|
194
|
+
actions,
|
|
195
|
+
active: live,
|
|
196
|
+
onTeardown(cleanup) {
|
|
197
|
+
if (live())
|
|
198
|
+
cleanups.push(cleanup);
|
|
199
|
+
else
|
|
200
|
+
cleanup();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
if (typeof returned === 'function') {
|
|
204
|
+
if (live())
|
|
205
|
+
cleanups.push(returned);
|
|
206
|
+
else
|
|
207
|
+
returned();
|
|
208
|
+
}
|
|
209
|
+
return () => teardownMount(token);
|
|
210
|
+
};
|
|
141
211
|
return {
|
|
142
212
|
core,
|
|
143
213
|
get props() {
|
|
@@ -165,7 +235,13 @@ function createElement(core, options = {}) {
|
|
|
165
235
|
listeners.delete(listener);
|
|
166
236
|
};
|
|
167
237
|
},
|
|
238
|
+
mount,
|
|
239
|
+
get mounted() {
|
|
240
|
+
return mountToken !== null;
|
|
241
|
+
},
|
|
168
242
|
destroy() {
|
|
243
|
+
if (mountToken)
|
|
244
|
+
teardownMount(mountToken);
|
|
169
245
|
destroyed = true;
|
|
170
246
|
for (const handle of timers)
|
|
171
247
|
clearTimeout(handle);
|
package/dist/cjs/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.FEE_LABEL_FALLBACK = exports.selectionTotals = exports.minorUnits = exports.expressesFee = exports.cartTotals = exports.manifestOf = exports.defineElement = exports.createElement = void 0;
|
|
17
|
+
exports.totalsAgree = exports.ticketItems = exports.ticketCount = exports.statusTone = exports.statusLabel = exports.resolveOccurrence = exports.orderTotals = exports.orderEventName = exports.orderCustomerEmail = exports.formatPrice = exports.formatDateTime = exports.formatDate = exports.errorMessage = exports.FEE_LABEL_FALLBACK = exports.selectionTotals = exports.minorUnits = exports.expressesFee = exports.cartTotals = exports.manifestOf = exports.NO_STORAGE = exports.NO_QUERY = exports.defineElement = exports.createElement = void 0;
|
|
18
18
|
/**
|
|
19
19
|
* `@ticketlayer/elements-core`: the headless cores the Ticketlayer Elements
|
|
20
20
|
* render.
|
|
@@ -33,6 +33,8 @@ exports.FEE_LABEL_FALLBACK = exports.selectionTotals = exports.minorUnits = expo
|
|
|
33
33
|
var define_js_1 = require("./define.js");
|
|
34
34
|
Object.defineProperty(exports, "createElement", { enumerable: true, get: function () { return define_js_1.createElement; } });
|
|
35
35
|
Object.defineProperty(exports, "defineElement", { enumerable: true, get: function () { return define_js_1.defineElement; } });
|
|
36
|
+
Object.defineProperty(exports, "NO_QUERY", { enumerable: true, get: function () { return define_js_1.NO_QUERY; } });
|
|
37
|
+
Object.defineProperty(exports, "NO_STORAGE", { enumerable: true, get: function () { return define_js_1.NO_STORAGE; } });
|
|
36
38
|
var manifest_js_1 = require("./manifest.js");
|
|
37
39
|
Object.defineProperty(exports, "manifestOf", { enumerable: true, get: function () { return manifest_js_1.manifestOf; } });
|
|
38
40
|
var money_js_1 = require("./money.js");
|
|
@@ -41,4 +43,18 @@ Object.defineProperty(exports, "expressesFee", { enumerable: true, get: function
|
|
|
41
43
|
Object.defineProperty(exports, "minorUnits", { enumerable: true, get: function () { return money_js_1.minorUnits; } });
|
|
42
44
|
Object.defineProperty(exports, "selectionTotals", { enumerable: true, get: function () { return money_js_1.selectionTotals; } });
|
|
43
45
|
Object.defineProperty(exports, "FEE_LABEL_FALLBACK", { enumerable: true, get: function () { return money_js_1.FEE_LABEL_FALLBACK; } });
|
|
46
|
+
var orders_js_1 = require("./orders.js");
|
|
47
|
+
Object.defineProperty(exports, "errorMessage", { enumerable: true, get: function () { return orders_js_1.errorMessage; } });
|
|
48
|
+
Object.defineProperty(exports, "formatDate", { enumerable: true, get: function () { return orders_js_1.formatDate; } });
|
|
49
|
+
Object.defineProperty(exports, "formatDateTime", { enumerable: true, get: function () { return orders_js_1.formatDateTime; } });
|
|
50
|
+
Object.defineProperty(exports, "formatPrice", { enumerable: true, get: function () { return orders_js_1.formatPrice; } });
|
|
51
|
+
Object.defineProperty(exports, "orderCustomerEmail", { enumerable: true, get: function () { return orders_js_1.orderCustomerEmail; } });
|
|
52
|
+
Object.defineProperty(exports, "orderEventName", { enumerable: true, get: function () { return orders_js_1.orderEventName; } });
|
|
53
|
+
Object.defineProperty(exports, "orderTotals", { enumerable: true, get: function () { return orders_js_1.orderTotals; } });
|
|
54
|
+
Object.defineProperty(exports, "resolveOccurrence", { enumerable: true, get: function () { return orders_js_1.resolveOccurrence; } });
|
|
55
|
+
Object.defineProperty(exports, "statusLabel", { enumerable: true, get: function () { return orders_js_1.statusLabel; } });
|
|
56
|
+
Object.defineProperty(exports, "statusTone", { enumerable: true, get: function () { return orders_js_1.statusTone; } });
|
|
57
|
+
Object.defineProperty(exports, "ticketCount", { enumerable: true, get: function () { return orders_js_1.ticketCount; } });
|
|
58
|
+
Object.defineProperty(exports, "ticketItems", { enumerable: true, get: function () { return orders_js_1.ticketItems; } });
|
|
59
|
+
Object.defineProperty(exports, "totalsAgree", { enumerable: true, get: function () { return orders_js_1.totalsAgree; } });
|
|
44
60
|
__exportStar(require("./cores/index.js"), exports);
|
package/dist/cjs/manifest.js
CHANGED
|
@@ -8,6 +8,7 @@ function manifestOf(core) {
|
|
|
8
8
|
name,
|
|
9
9
|
attr: spec.attr,
|
|
10
10
|
type: spec.type,
|
|
11
|
+
tsType: spec.tsType ?? null,
|
|
11
12
|
default: spec.default ?? null,
|
|
12
13
|
required: !!spec.required,
|
|
13
14
|
docs: spec.docs,
|
|
@@ -28,6 +29,7 @@ function manifestOf(core) {
|
|
|
28
29
|
strings: { ...core.strings },
|
|
29
30
|
statuses: [...core.statuses],
|
|
30
31
|
actions: Object.keys(dryRunActions(core)).sort(),
|
|
32
|
+
mounts: typeof core.mount === 'function',
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
@@ -50,5 +52,8 @@ function dryRunActions(core) {
|
|
|
50
52
|
client: refuse('client'),
|
|
51
53
|
optionalClient: refuse('optionalClient'),
|
|
52
54
|
after: refuse('after'),
|
|
55
|
+
storage: { get: refuse('storage.get'), set: refuse('storage.set') },
|
|
56
|
+
query: { get: refuse('query.get') },
|
|
57
|
+
whenClient: refuse('whenClient'),
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatPrice = formatPrice;
|
|
4
|
+
exports.formatDateTime = formatDateTime;
|
|
5
|
+
exports.formatDate = formatDate;
|
|
6
|
+
exports.ticketItems = ticketItems;
|
|
7
|
+
exports.ticketCount = ticketCount;
|
|
8
|
+
exports.orderEventName = orderEventName;
|
|
9
|
+
exports.orderCustomerEmail = orderCustomerEmail;
|
|
10
|
+
exports.statusLabel = statusLabel;
|
|
11
|
+
exports.statusTone = statusTone;
|
|
12
|
+
exports.errorMessage = errorMessage;
|
|
13
|
+
exports.resolveOccurrence = resolveOccurrence;
|
|
14
|
+
exports.orderTotals = orderTotals;
|
|
15
|
+
exports.totalsAgree = totalsAgree;
|
|
16
|
+
/**
|
|
17
|
+
* The order shape, and what every harness has to work out from it.
|
|
18
|
+
*
|
|
19
|
+
* The sales order view carries line items, totals and tickets but no event
|
|
20
|
+
* name, no occurrence date and often no customer email, so these derive what
|
|
21
|
+
* they can from the order and resolve the rest through the client. They lived
|
|
22
|
+
* in `packages/elements/src/utils/order-summary.ts`, where only the web could
|
|
23
|
+
* reach them; they moved here with the split of `tl-my-orders` and
|
|
24
|
+
* `tl-order-confirmation` (TKT-69) so the same rules answer on every runtime.
|
|
25
|
+
* That module re-exports them, so every existing import still resolves and
|
|
26
|
+
* `tl-order-tickets` and `tl-ticket`, which are not split yet, read the same
|
|
27
|
+
* helpers they always did.
|
|
28
|
+
*
|
|
29
|
+
* `en-GB` is written out in the date and money helpers rather than taken from
|
|
30
|
+
* the instance's locale. That is what the components did and this is a
|
|
31
|
+
* behaviour-identical move; `ctx.formatMoney` is the locale-aware formatter and
|
|
32
|
+
* these are deliberately not it, because an order's money arrives as a minor
|
|
33
|
+
* unit integer OR a decimal string and `formatMoney` takes only the first.
|
|
34
|
+
*/
|
|
35
|
+
const money_js_1 = require("./money.js");
|
|
36
|
+
/** Money arrives as minor-unit integers or decimal strings ("50.00"). */
|
|
37
|
+
function formatPrice(value, currency = 'GBP') {
|
|
38
|
+
if (value === null || value === undefined || value === '')
|
|
39
|
+
return '';
|
|
40
|
+
const fmt = new Intl.NumberFormat('en-GB', { style: 'currency', currency });
|
|
41
|
+
if (typeof value === 'number')
|
|
42
|
+
return fmt.format(value / 100);
|
|
43
|
+
const n = Number(value);
|
|
44
|
+
return Number.isNaN(n) ? String(value) : fmt.format(n);
|
|
45
|
+
}
|
|
46
|
+
function formatDateTime(iso) {
|
|
47
|
+
return new Date(iso).toLocaleDateString('en-GB', {
|
|
48
|
+
weekday: 'short',
|
|
49
|
+
day: 'numeric',
|
|
50
|
+
month: 'short',
|
|
51
|
+
year: 'numeric',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function formatDate(iso) {
|
|
57
|
+
return new Date(iso).toLocaleDateString('en-GB', {
|
|
58
|
+
day: 'numeric',
|
|
59
|
+
month: 'short',
|
|
60
|
+
year: 'numeric',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Line items that are tickets (fees, taxes and discounts have their own type). */
|
|
64
|
+
function ticketItems(order) {
|
|
65
|
+
return (order.items ?? []).filter((i) => !i.type || i.type === 'ticket');
|
|
66
|
+
}
|
|
67
|
+
function ticketCount(order) {
|
|
68
|
+
const fromItems = ticketItems(order).reduce((sum, i) => sum + (Number(i.quantity) || 0), 0);
|
|
69
|
+
return fromItems || order.tickets?.length || 0;
|
|
70
|
+
}
|
|
71
|
+
/** Event name as the order view exposes it: the first ticket line's name. */
|
|
72
|
+
function orderEventName(order) {
|
|
73
|
+
return order.eventName || ticketItems(order)[0]?.name || order.items?.[0]?.name || null;
|
|
74
|
+
}
|
|
75
|
+
function orderCustomerEmail(order, customer) {
|
|
76
|
+
const fromOrder = order?.customerEmail || order?.customer?.email;
|
|
77
|
+
if (fromOrder)
|
|
78
|
+
return fromOrder;
|
|
79
|
+
const c = customer;
|
|
80
|
+
return c?.email || null;
|
|
81
|
+
}
|
|
82
|
+
function statusLabel(status) {
|
|
83
|
+
if (!status)
|
|
84
|
+
return '';
|
|
85
|
+
return status.replace(/_/g, ' ').replace(/^\w/, (m) => m.toUpperCase());
|
|
86
|
+
}
|
|
87
|
+
function statusTone(status) {
|
|
88
|
+
switch (status) {
|
|
89
|
+
case 'confirmed':
|
|
90
|
+
return 'success';
|
|
91
|
+
case 'completed':
|
|
92
|
+
case 'fulfilled':
|
|
93
|
+
return 'info';
|
|
94
|
+
case 'pending':
|
|
95
|
+
case 'processing':
|
|
96
|
+
return 'warning';
|
|
97
|
+
case 'cancelled':
|
|
98
|
+
case 'expired':
|
|
99
|
+
case 'failed':
|
|
100
|
+
return 'error';
|
|
101
|
+
default:
|
|
102
|
+
return 'muted';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function errorMessage(err, fallback) {
|
|
106
|
+
return err instanceof Error && err.message ? err.message : fallback;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find the event and start time for an occurrence id. The order view only
|
|
110
|
+
* carries the occurrence id, so this walks the channel's listings (cached by
|
|
111
|
+
* live-api) until one owns the occurrence. Best effort: null when not found,
|
|
112
|
+
* and null on a client whose events manager cannot list or fetch, which is an
|
|
113
|
+
* older connector rather than a failure.
|
|
114
|
+
*/
|
|
115
|
+
async function resolveOccurrence(client, occurrenceId, maxEvents = 12) {
|
|
116
|
+
const events = client.getEventsManager();
|
|
117
|
+
if (!events?.list || !events.get)
|
|
118
|
+
return null;
|
|
119
|
+
const listing = await events.list({ limit: maxEvents });
|
|
120
|
+
const items = (listing?.items ?? []);
|
|
121
|
+
const candidates = items.slice(0, maxEvents);
|
|
122
|
+
for (const candidate of candidates) {
|
|
123
|
+
const detail = (await events.get(candidate.id).catch(() => null));
|
|
124
|
+
const occ = detail?.occurrences?.find((o) => o.id === occurrenceId);
|
|
125
|
+
if (occ) {
|
|
126
|
+
return {
|
|
127
|
+
eventId: detail?.id ?? candidate.id,
|
|
128
|
+
eventName: detail?.name ?? candidate.name ?? '',
|
|
129
|
+
startsAt: occ.startsAt ?? null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function orderTotals(order) {
|
|
136
|
+
const o = order || {};
|
|
137
|
+
const items = Array.isArray(o.items) ? o.items : [];
|
|
138
|
+
const feeLines = items.filter((i) => i.type === 'fee');
|
|
139
|
+
const discountLines = items.filter((i) => i.type === 'discount');
|
|
140
|
+
const tickets = ticketItems(o);
|
|
141
|
+
const feeFromLines = feeLines.length
|
|
142
|
+
? feeLines.reduce((sum, i) => sum + ((0, money_js_1.minorUnits)(i.subtotal) ?? 0), 0)
|
|
143
|
+
: null;
|
|
144
|
+
const fees = feeFromLines ?? (0, money_js_1.minorUnits)(o.totalFees) ?? 0;
|
|
145
|
+
const declaredSubtotal = (0, money_js_1.minorUnits)(o.subtotal);
|
|
146
|
+
const subtotal = tickets.length
|
|
147
|
+
? tickets.reduce((sum, i) => sum + ((0, money_js_1.minorUnits)(i.subtotal) ?? 0), 0)
|
|
148
|
+
: (declaredSubtotal ?? 0) - fees;
|
|
149
|
+
const discountFromLines = discountLines.length
|
|
150
|
+
? discountLines.reduce((sum, i) => sum + Math.abs((0, money_js_1.minorUnits)(i.subtotal) ?? 0), 0)
|
|
151
|
+
: null;
|
|
152
|
+
const discount = (0, money_js_1.minorUnits)(o.totalDiscounts) ?? discountFromLines ?? 0;
|
|
153
|
+
const tax = (0, money_js_1.minorUnits)(o.totalTax) ?? 0;
|
|
154
|
+
const named = feeLines.some((i) => typeof i.name === 'string' && !!i.name);
|
|
155
|
+
return {
|
|
156
|
+
currency: o.currency || 'GBP',
|
|
157
|
+
subtotal,
|
|
158
|
+
fees,
|
|
159
|
+
feeLabel: feeLines.map((i) => i.name).find((n) => !!n) || money_js_1.FEE_LABEL_FALLBACK,
|
|
160
|
+
showFee: (0, money_js_1.expressesFee)(feeFromLines ?? (0, money_js_1.minorUnits)(o.totalFees), named),
|
|
161
|
+
tax,
|
|
162
|
+
discount,
|
|
163
|
+
total: (0, money_js_1.minorUnits)(o.total) ?? subtotal + fees + tax - discount,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** The rows add up to the total the order reports. Asserted in the specs. */
|
|
167
|
+
function totalsAgree(totals) {
|
|
168
|
+
return totals.subtotal + totals.fees + totals.tax - totals.discount === totals.total;
|
|
169
|
+
}
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -14,12 +14,118 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export interface ElementEventsManager {
|
|
16
16
|
getTicketTypes(eventId: string, occurrenceId: string): Promise<unknown[]>;
|
|
17
|
+
/** One event's detail. Absent on a connector that only sells. */
|
|
18
|
+
get?(eventId: string): Promise<unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* The channel's listing. Absent on a connector that only sells, so every
|
|
21
|
+
* call site checks: `tl-event-list` renders it, and `resolveOccurrence`
|
|
22
|
+
* walks it to put an event name on an order.
|
|
23
|
+
*/
|
|
24
|
+
list?(options?: {
|
|
25
|
+
limit?: number;
|
|
26
|
+
category?: string;
|
|
27
|
+
}): Promise<unknown>;
|
|
17
28
|
}
|
|
18
29
|
export interface ElementCartManager {
|
|
19
30
|
addItem(ticketTypeId: string, quantity: number, occurrenceId: string): Promise<unknown>;
|
|
31
|
+
/** The cart as it stands. Absent on a connector that only writes. */
|
|
32
|
+
get?(): Promise<unknown>;
|
|
33
|
+
removeItem?(itemId: string): Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to a cart event, returning the unsubscribe.
|
|
36
|
+
*
|
|
37
|
+
* Separate from {@link ElementClient.on} on purpose, and not a duplicate of
|
|
38
|
+
* it: `@ticketlayer/live`'s connector adapts the payload on the cart
|
|
39
|
+
* manager's bus (it adds `itemCount` and the derived money rows) and passes
|
|
40
|
+
* it through untouched on the client's. A cart core reads those figures, so
|
|
41
|
+
* it subscribes here, and `tl-cart-drawer`, which only listens for a UI
|
|
42
|
+
* intent, subscribes on the client.
|
|
43
|
+
*/
|
|
44
|
+
on?(event: string, callback: (data: unknown) => void): (() => void) | void;
|
|
45
|
+
}
|
|
46
|
+
/** What a presale or promo code redemption gives back. */
|
|
47
|
+
export interface ElementPresaleRedemption {
|
|
48
|
+
benefit: string;
|
|
49
|
+
alreadyHeld: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface ElementPresaleManager {
|
|
52
|
+
redeem(code: string): Promise<ElementPresaleRedemption>;
|
|
53
|
+
}
|
|
54
|
+
/** What a core may read and do about the customer's own orders. */
|
|
55
|
+
export interface ElementOrdersManager {
|
|
56
|
+
/** The signed-in customer's orders. */
|
|
57
|
+
mine(): Promise<unknown>;
|
|
58
|
+
/** One order, with its tickets. Order-access gated by the session. */
|
|
59
|
+
get(orderId: string): Promise<unknown>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Magic-link sign-in. `customer` and `authenticated` are read as properties
|
|
63
|
+
* rather than called, because the connector exposes them as live getters over
|
|
64
|
+
* the session and a core reads them again after every `auth:changed`.
|
|
65
|
+
*/
|
|
66
|
+
export interface ElementAuthManager {
|
|
67
|
+
readonly customer: unknown | null;
|
|
68
|
+
readonly authenticated: boolean;
|
|
69
|
+
requestMagicLink(contact: {
|
|
70
|
+
email?: string;
|
|
71
|
+
phone?: string;
|
|
72
|
+
}): Promise<unknown>;
|
|
73
|
+
exchange(code: string): Promise<unknown>;
|
|
74
|
+
logout(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/** What a completed purchase hands back to the element that opened the modal. */
|
|
77
|
+
export interface ElementOrderResult {
|
|
78
|
+
orderId: string;
|
|
79
|
+
orderNumber: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* What a core passes when it asks the host to open a modal.
|
|
83
|
+
*
|
|
84
|
+
* The callbacks are how the modal reports back: the element that opened it is
|
|
85
|
+
* the one that announces the outcome to the page, which is why the wrapper
|
|
86
|
+
* elements have events at all.
|
|
87
|
+
*/
|
|
88
|
+
export interface ElementModalOptions {
|
|
89
|
+
/** Open straight on one occurrence, skipping the occurrence selector. */
|
|
90
|
+
occurrenceId?: string;
|
|
91
|
+
onComplete?(order: ElementOrderResult): void;
|
|
92
|
+
onError?(error: Error): void;
|
|
93
|
+
onClose?(): void;
|
|
94
|
+
}
|
|
95
|
+
/** The fluent per-event accessor: `client.event(id).openModal(...)`. */
|
|
96
|
+
export interface ElementEventContext {
|
|
97
|
+
openModal(options?: ElementModalOptions): void;
|
|
98
|
+
}
|
|
99
|
+
/** The checkout modal verbs. */
|
|
100
|
+
export interface ElementCheckoutContext {
|
|
101
|
+
openModal(options?: ElementModalOptions): void;
|
|
102
|
+
closeModal(): void;
|
|
20
103
|
}
|
|
21
104
|
export interface ElementClient {
|
|
22
105
|
getEventsManager(): ElementEventsManager;
|
|
23
106
|
/** Absent on an older connector, so every call site checks. */
|
|
24
107
|
getCartManager?(): ElementCartManager | null | undefined;
|
|
108
|
+
/** Presale and promo codes. Absent on an older connector. */
|
|
109
|
+
getPresaleManager?(): ElementPresaleManager | null | undefined;
|
|
110
|
+
/** The customer's orders and their tickets. Absent on an older connector. */
|
|
111
|
+
getOrdersManager?(): ElementOrdersManager | null | undefined;
|
|
112
|
+
/** Magic-link sign-in. Absent on an older connector. */
|
|
113
|
+
getAuthManager?(): ElementAuthManager | null | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The purchase modal for one event, as a UI intent the host's client relays.
|
|
116
|
+
* Absent on an older connector, so every call site checks.
|
|
117
|
+
*/
|
|
118
|
+
event?(eventId: string): ElementEventContext;
|
|
119
|
+
/**
|
|
120
|
+
* The checkout modal, the same way. Absent on an older connector.
|
|
121
|
+
*/
|
|
122
|
+
checkout?: ElementCheckoutContext;
|
|
123
|
+
/**
|
|
124
|
+
* Subscribe to a client event (`cart:updated` and the rest), returning the
|
|
125
|
+
* unsubscribe. This is how a core that lives on a subscription binds it in
|
|
126
|
+
* its own `mount` and hands the unsubscribe to `ctx.onTeardown`, rather than
|
|
127
|
+
* every harness re-implementing subscribe-on-connect per runtime (TKT-197).
|
|
128
|
+
* Optional, so a connector without a bus is tolerated rather than assumed.
|
|
129
|
+
*/
|
|
130
|
+
on?(event: string, callback: (data: unknown) => void): (() => void) | void;
|
|
25
131
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The four states a seller puts this button in. */
|
|
2
|
+
export type BuyTicketsButtonSaleState = 'available' | 'soldOut' | 'waitlist' | 'comingSoon';
|
|
3
|
+
export type BuyTicketsButtonVariant = 'primary' | 'secondary' | 'outline';
|
|
4
|
+
export type BuyTicketsButtonSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
export interface BuyTicketsButtonProps {
|
|
6
|
+
eventId: string;
|
|
7
|
+
occurrenceId: string | undefined;
|
|
8
|
+
state: BuyTicketsButtonSaleState;
|
|
9
|
+
label: string | undefined;
|
|
10
|
+
variant: BuyTicketsButtonVariant;
|
|
11
|
+
size: BuyTicketsButtonSize;
|
|
12
|
+
fullWidth: boolean;
|
|
13
|
+
price: number | undefined;
|
|
14
|
+
currency: string;
|
|
15
|
+
pricePrefix: string;
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Nothing. The modal belongs to the client, the sale state is the host's to
|
|
20
|
+
* set, and hover is the harness's: a pointer is not a fact every runtime has,
|
|
21
|
+
* the same way Enter and Space are not.
|
|
22
|
+
*/
|
|
23
|
+
export type BuyTicketsButtonState = Record<string, never>;
|
|
24
|
+
export interface BuyTicketsButtonDerived {
|
|
25
|
+
/** Whether a press should do anything. The harness draws the rest from it. */
|
|
26
|
+
interactive: boolean;
|
|
27
|
+
/** The label on the button: the host's override, else the state's own. */
|
|
28
|
+
buttonLabel: string;
|
|
29
|
+
/**
|
|
30
|
+
* The class the sale state is rendered under. The core names the state and
|
|
31
|
+
* the stylesheet says which token family that class paints with, because a
|
|
32
|
+
* core cannot map a status onto one (`tl-event-card`'s status badge is the
|
|
33
|
+
* same bargain).
|
|
34
|
+
*/
|
|
35
|
+
stateClass: string;
|
|
36
|
+
/** The formatted price, or null when the host gave none to show. */
|
|
37
|
+
priceLabel: string | null;
|
|
38
|
+
}
|
|
39
|
+
export type BuyTicketsButtonStatus = 'ready' | 'disabled';
|
|
40
|
+
declare const STRINGS: {
|
|
41
|
+
available: string;
|
|
42
|
+
soldOut: string;
|
|
43
|
+
waitlist: string;
|
|
44
|
+
comingSoon: string;
|
|
45
|
+
};
|
|
46
|
+
export type BuyTicketsButtonStrings = typeof STRINGS;
|
|
47
|
+
export declare const buyTicketsButtonCore: import("../define.js").ElementCore<BuyTicketsButtonProps, BuyTicketsButtonState, BuyTicketsButtonDerived, {
|
|
48
|
+
activate(): void;
|
|
49
|
+
}, BuyTicketsButtonStatus, {
|
|
50
|
+
available: string;
|
|
51
|
+
soldOut: string;
|
|
52
|
+
waitlist: string;
|
|
53
|
+
comingSoon: string;
|
|
54
|
+
}>;
|
|
55
|
+
export {};
|