@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.orderConfirmationCore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-order-confirmation`: the thank-you block - reference, email, event and
|
|
6
|
+
* date, the ticket lines, the money rows, and the tickets themselves.
|
|
7
|
+
*
|
|
8
|
+
* It finds its order three ways, in this order, and that priority is the whole
|
|
9
|
+
* point of the element:
|
|
10
|
+
*
|
|
11
|
+
* 1. the `order-id` prop, for a host that already knows;
|
|
12
|
+
* 2. the query the page was opened with (`?orderId=`), which is the contract
|
|
13
|
+
* the hosted box office confirmation page uses, plus `?redirect_status=`,
|
|
14
|
+
* which is how a payment provider says on the way back whether the money
|
|
15
|
+
* actually moved;
|
|
16
|
+
* 3. the client's `checkout:completed`, for a page that mounts this before
|
|
17
|
+
* the customer has paid.
|
|
18
|
+
*
|
|
19
|
+
* (2) is why this core reads {@link ActionContext.query} and (3) is why it
|
|
20
|
+
* holds a subscription, bound in its own `mount` and handed to
|
|
21
|
+
* `ctx.onTeardown` (TKT-197). It is the last of the eighteen elements to live
|
|
22
|
+
* on a subscription.
|
|
23
|
+
*
|
|
24
|
+
* Behaviour is what the Stencil component did, to the letter, including:
|
|
25
|
+
*
|
|
26
|
+
* - a redirect that is not `succeeded` shows "Payment not completed" and does
|
|
27
|
+
* NOT read the order, even when there is an order id to read.
|
|
28
|
+
* - a re-read while an order is already on screen does not blank it: the
|
|
29
|
+
* skeleton is only for the first read.
|
|
30
|
+
* - a failed read keeps the thank-you header and puts the message underneath,
|
|
31
|
+
* because someone who has just paid should not be told the purchase failed.
|
|
32
|
+
* - `checkout:completed` shows what it carries at once and THEN re-reads,
|
|
33
|
+
* because an embed's payload is only the id and the order number until the
|
|
34
|
+
* tickets have been issued.
|
|
35
|
+
* - the event name and date are best effort. The order view carries neither,
|
|
36
|
+
* so they are walked out of the channel's listing, and a failure there
|
|
37
|
+
* leaves the line at whatever the order itself said.
|
|
38
|
+
*
|
|
39
|
+
* What did not come across is the `console.error` tracing, which a library
|
|
40
|
+
* should not do for its host (`tl-cart` dropped its own for the same reason).
|
|
41
|
+
*/
|
|
42
|
+
const define_js_1 = require("../define.js");
|
|
43
|
+
const orders_js_1 = require("../orders.js");
|
|
44
|
+
const STRINGS = {
|
|
45
|
+
paymentFailedTitle: 'Payment not completed',
|
|
46
|
+
paymentFailedLead: "Your payment wasn't completed. Your cart is still saved, so you can try again.",
|
|
47
|
+
title: "You're going!",
|
|
48
|
+
leadPrefix: 'Order ',
|
|
49
|
+
leadSuffix: ' is confirmed. Your tickets are on their way to your email.',
|
|
50
|
+
leadGeneric: 'Your order is confirmed. Your tickets are on their way to your email.',
|
|
51
|
+
errorPrefix: "We couldn't load your order details yet. ",
|
|
52
|
+
rowReference: 'Order reference',
|
|
53
|
+
rowEmail: 'Email',
|
|
54
|
+
rowEvent: 'Event',
|
|
55
|
+
rowDate: 'Date',
|
|
56
|
+
subtotalRow: 'Subtotal',
|
|
57
|
+
taxRow: 'Tax',
|
|
58
|
+
discountRow: 'Discount',
|
|
59
|
+
totalRow: 'Total',
|
|
60
|
+
ticketsTitle: 'Your tickets',
|
|
61
|
+
ticketsHint: 'Show these QR codes at the door for entry. Use Download PDF on a ticket to save it.',
|
|
62
|
+
noOrdersManager: 'Orders are not available',
|
|
63
|
+
notFound: 'Order not found',
|
|
64
|
+
loadFailed: 'Failed to load your order',
|
|
65
|
+
};
|
|
66
|
+
/** The query parameter a payment provider comes back on, and the one value that means paid. */
|
|
67
|
+
const REDIRECT_STATUS = 'redirect_status';
|
|
68
|
+
const REDIRECT_SUCCEEDED = 'succeeded';
|
|
69
|
+
exports.orderConfirmationCore = (0, define_js_1.defineElement)({
|
|
70
|
+
tag: 'tl-order-confirmation',
|
|
71
|
+
docs: 'The thank-you block: order reference, email, event and date, the ticket lines, the money rows and the tickets.',
|
|
72
|
+
category: 'checkout',
|
|
73
|
+
runtimes: ['web', 'native'],
|
|
74
|
+
tokens: [
|
|
75
|
+
'border',
|
|
76
|
+
'error',
|
|
77
|
+
'font-family',
|
|
78
|
+
'font-family-mono',
|
|
79
|
+
'foreground',
|
|
80
|
+
'muted',
|
|
81
|
+
'muted-foreground',
|
|
82
|
+
'radius-lg',
|
|
83
|
+
'radius-md',
|
|
84
|
+
'secondary',
|
|
85
|
+
'spacing-1',
|
|
86
|
+
'spacing-2',
|
|
87
|
+
'spacing-3',
|
|
88
|
+
'spacing-4',
|
|
89
|
+
'spacing-6',
|
|
90
|
+
'success',
|
|
91
|
+
'success-foreground',
|
|
92
|
+
],
|
|
93
|
+
sdkMethods: ['orders.get', 'events.list', 'events.get', 'auth.customer', 'on'],
|
|
94
|
+
props: {
|
|
95
|
+
orderId: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
tsType: 'string | undefined',
|
|
98
|
+
attr: 'order-id',
|
|
99
|
+
docs: 'The order to show. Falls back to `?orderId=` in the page URL.',
|
|
100
|
+
},
|
|
101
|
+
customerEmail: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
tsType: 'string | undefined',
|
|
104
|
+
attr: 'customer-email',
|
|
105
|
+
docs: 'Email to show when the order view carries none (a host that collected it at checkout).',
|
|
106
|
+
},
|
|
107
|
+
showTickets: {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
attr: 'show-tickets',
|
|
110
|
+
default: true,
|
|
111
|
+
docs: 'Mount tl-order-tickets under the summary (default true).',
|
|
112
|
+
},
|
|
113
|
+
columns: { type: 'number', attr: 'columns', default: 2, docs: 'Columns for the ticket grid.' },
|
|
114
|
+
},
|
|
115
|
+
events: {
|
|
116
|
+
tlOrderLoaded: { detail: '{ order: TlOrderData; }', docs: 'the order was loaded' },
|
|
117
|
+
tlError: { detail: '{ message: string; }', docs: 'the order could not be loaded' },
|
|
118
|
+
},
|
|
119
|
+
strings: STRINGS,
|
|
120
|
+
statuses: ['payment-failed', 'idle', 'loading', 'ready'],
|
|
121
|
+
state: () => ({
|
|
122
|
+
order: null,
|
|
123
|
+
resolvedOrderId: null,
|
|
124
|
+
loading: false,
|
|
125
|
+
error: null,
|
|
126
|
+
paymentFailed: false,
|
|
127
|
+
occurrence: null,
|
|
128
|
+
signedInCustomer: null,
|
|
129
|
+
}),
|
|
130
|
+
/**
|
|
131
|
+
* On screen: read the link, start the read, subscribe.
|
|
132
|
+
*
|
|
133
|
+
* The query goes first because it decides whether there is anything to read
|
|
134
|
+
* at all: a payment that did not go through is not an order to fetch. The
|
|
135
|
+
* read is started and not awaited before subscribing, so a
|
|
136
|
+
* `checkout:completed` that lands mid-read is not missed - the component
|
|
137
|
+
* read in `componentWillLoad` and subscribed in `componentDidLoad`, and had
|
|
138
|
+
* exactly that window.
|
|
139
|
+
*
|
|
140
|
+
* The subscription needs the client and the read does not: `load` waits for
|
|
141
|
+
* the client itself, inside the try block that announces a failure, which is
|
|
142
|
+
* why a page that never starts Live still emits `tlError` here when it was
|
|
143
|
+
* given an order to show.
|
|
144
|
+
*/
|
|
145
|
+
mount: (ctx) => {
|
|
146
|
+
void (async () => {
|
|
147
|
+
// The link, before anything else: a failed redirect means no read.
|
|
148
|
+
const redirect = ctx.query.get(REDIRECT_STATUS);
|
|
149
|
+
ctx.setState({
|
|
150
|
+
paymentFailed: !!redirect && redirect !== REDIRECT_SUCCEEDED,
|
|
151
|
+
resolvedOrderId: ctx.props.orderId || ctx.query.get('orderId') || null,
|
|
152
|
+
});
|
|
153
|
+
const reading = ctx.state.resolvedOrderId && !ctx.state.paymentFailed
|
|
154
|
+
? ctx.actions.load()
|
|
155
|
+
: Promise.resolve();
|
|
156
|
+
let client = null;
|
|
157
|
+
try {
|
|
158
|
+
client = await ctx.whenClient();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
client = null;
|
|
162
|
+
}
|
|
163
|
+
if (client && ctx.active()) {
|
|
164
|
+
const off = client.on?.('checkout:completed', (payload) => {
|
|
165
|
+
ctx.actions.checkoutCompleted(payload);
|
|
166
|
+
});
|
|
167
|
+
if (typeof off === 'function')
|
|
168
|
+
ctx.onTeardown(off);
|
|
169
|
+
}
|
|
170
|
+
await reading;
|
|
171
|
+
})();
|
|
172
|
+
},
|
|
173
|
+
status: ({ state }) => state.paymentFailed
|
|
174
|
+
? 'payment-failed'
|
|
175
|
+
: !state.resolvedOrderId && !state.order
|
|
176
|
+
? 'idle'
|
|
177
|
+
: state.loading
|
|
178
|
+
? 'loading'
|
|
179
|
+
: 'ready',
|
|
180
|
+
derive: ({ state, props, strings }) => {
|
|
181
|
+
const order = state.order;
|
|
182
|
+
const totals = (0, orders_js_1.orderTotals)(order);
|
|
183
|
+
const currency = totals.currency;
|
|
184
|
+
const email = props.customerEmail || (0, orders_js_1.orderCustomerEmail)(order, state.signedInCustomer);
|
|
185
|
+
const eventName = state.occurrence?.eventName || (order ? (0, orders_js_1.orderEventName)(order) : null);
|
|
186
|
+
const startsAt = state.occurrence?.startsAt;
|
|
187
|
+
const rows = [];
|
|
188
|
+
if (order)
|
|
189
|
+
rows.push({ label: strings.rowReference, value: order.orderNumber, reference: true });
|
|
190
|
+
if (email)
|
|
191
|
+
rows.push({ label: strings.rowEmail, value: email, reference: false });
|
|
192
|
+
if (eventName)
|
|
193
|
+
rows.push({ label: strings.rowEvent, value: eventName, reference: false });
|
|
194
|
+
if (startsAt)
|
|
195
|
+
rows.push({ label: strings.rowDate, value: (0, orders_js_1.formatDateTime)(startsAt), reference: false });
|
|
196
|
+
return {
|
|
197
|
+
leadText: order?.orderNumber
|
|
198
|
+
? `${strings.leadPrefix}${order.orderNumber}${strings.leadSuffix}`
|
|
199
|
+
: strings.leadGeneric,
|
|
200
|
+
rows,
|
|
201
|
+
lines: order
|
|
202
|
+
? (0, orders_js_1.ticketItems)(order).map((item) => ({
|
|
203
|
+
label: `${item.quantity} x ${item.name}`,
|
|
204
|
+
priceLabel: (0, orders_js_1.formatPrice)(item.subtotal, order.currency),
|
|
205
|
+
}))
|
|
206
|
+
: [],
|
|
207
|
+
totals,
|
|
208
|
+
subtotalLabel: (0, orders_js_1.formatPrice)(totals.subtotal, currency),
|
|
209
|
+
feeAmountLabel: (0, orders_js_1.formatPrice)(totals.fees, currency),
|
|
210
|
+
taxLabel: (0, orders_js_1.formatPrice)(totals.tax, currency),
|
|
211
|
+
discountLabel: (0, orders_js_1.formatPrice)(totals.discount, currency),
|
|
212
|
+
totalLabel: (0, orders_js_1.formatPrice)(totals.total, currency),
|
|
213
|
+
showTaxRow: totals.tax > 0,
|
|
214
|
+
showDiscountRow: totals.discount > 0,
|
|
215
|
+
// An embed's checkout:completed carries only id and order number until
|
|
216
|
+
// the refetch lands, so there is nothing to summarise yet.
|
|
217
|
+
showSummary: !!order && Array.isArray(order.items),
|
|
218
|
+
showError: !!state.error && !order,
|
|
219
|
+
showTickets: props.showTickets && !!order,
|
|
220
|
+
ticketsOrderId: order?.id || state.resolvedOrderId || '',
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
actions: (ctx) => {
|
|
224
|
+
/**
|
|
225
|
+
* Put an event name and a start time on the summary. The order view has
|
|
226
|
+
* neither, so the occurrence is walked out of the channel's listing. Best
|
|
227
|
+
* effort and never blocking: a failure leaves the line at what the order
|
|
228
|
+
* said, and a result that arrives after the order changed underneath is
|
|
229
|
+
* dropped.
|
|
230
|
+
*/
|
|
231
|
+
const resolveEvent = async (client, order) => {
|
|
232
|
+
const occurrenceId = order.tickets?.find((t) => t.eventOccurrenceId)?.eventOccurrenceId;
|
|
233
|
+
if (!occurrenceId)
|
|
234
|
+
return;
|
|
235
|
+
try {
|
|
236
|
+
const resolved = await (0, orders_js_1.resolveOccurrence)(client, occurrenceId);
|
|
237
|
+
if (resolved && ctx.state.order?.id === order.id)
|
|
238
|
+
ctx.setState({ occurrence: resolved });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Leave the event line at what the order gives us.
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const self = {
|
|
245
|
+
/** Read the order. The skeleton is only for the first read. */
|
|
246
|
+
async load() {
|
|
247
|
+
const id = ctx.state.resolvedOrderId;
|
|
248
|
+
if (!id)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
ctx.setState({ loading: !ctx.state.order, error: null });
|
|
252
|
+
const client = await ctx.whenClient();
|
|
253
|
+
const orders = client.getOrdersManager?.();
|
|
254
|
+
if (!orders)
|
|
255
|
+
throw new Error(ctx.strings.noOrdersManager);
|
|
256
|
+
const order = (await orders.get(id));
|
|
257
|
+
if (!order)
|
|
258
|
+
throw new Error(ctx.strings.notFound);
|
|
259
|
+
ctx.setState({
|
|
260
|
+
order,
|
|
261
|
+
signedInCustomer: client.getAuthManager?.()?.customer ?? null,
|
|
262
|
+
});
|
|
263
|
+
ctx.emit('tlOrderLoaded', { order });
|
|
264
|
+
void resolveEvent(client, order);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
const message = (0, orders_js_1.errorMessage)(err, ctx.strings.loadFailed);
|
|
268
|
+
ctx.setState({ error: message });
|
|
269
|
+
ctx.emit('tlError', { message });
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
ctx.setState({ loading: false });
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
/**
|
|
276
|
+
* The host pointed the element at a different order. It resolves again
|
|
277
|
+
* the way the mount did - the prop first, the link second - and reads.
|
|
278
|
+
* Not a remount: the `checkout:completed` subscription belongs to the
|
|
279
|
+
* element, not to the order it happens to be showing.
|
|
280
|
+
*/
|
|
281
|
+
orderIdChanged() {
|
|
282
|
+
ctx.setState({ resolvedOrderId: ctx.props.orderId || ctx.query.get('orderId') || null });
|
|
283
|
+
void self.load();
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* The client finished a checkout. Show what it carries at once, because
|
|
287
|
+
* an order number on screen beats a skeleton, then read the order again:
|
|
288
|
+
* the tickets are issued after the order exists, so the payload almost
|
|
289
|
+
* never has them.
|
|
290
|
+
*/
|
|
291
|
+
checkoutCompleted(order) {
|
|
292
|
+
if (!order?.id)
|
|
293
|
+
return;
|
|
294
|
+
ctx.setState({ paymentFailed: false, order, resolvedOrderId: order.id });
|
|
295
|
+
ctx.emit('tlOrderLoaded', { order });
|
|
296
|
+
void self.load();
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return self;
|
|
300
|
+
},
|
|
301
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.orderTicketsCore = exports.POLL_DELAYS_MS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-order-tickets`: every ticket in an order, as scannable cards.
|
|
6
|
+
*
|
|
7
|
+
* The one core that waits rather than concluding. Tickets are issued
|
|
8
|
+
* asynchronously - the payment confirms, the order is marked paid, and
|
|
9
|
+
* entitlement issuance follows a moment later - so a confirmation page renders
|
|
10
|
+
* BEFORE the passes exist, reliably rather than occasionally. The element used
|
|
11
|
+
* to answer that with "no tickets for this order yet", which is true and reads
|
|
12
|
+
* as "something went wrong" to somebody who has just been charged. Refreshing
|
|
13
|
+
* showed them, so the only thing missing was the waiting.
|
|
14
|
+
*
|
|
15
|
+
* So an empty order is treated as issuance in flight and polled, with the gaps
|
|
16
|
+
* widening, for about a minute. Then it stops and says the tickets are coming
|
|
17
|
+
* by email, because at that point something probably is wrong and an element
|
|
18
|
+
* that polls forever is worse than one that admits it.
|
|
19
|
+
*
|
|
20
|
+
* The polling belongs to the mount, which is what the split adds: the
|
|
21
|
+
* component cancelled its timer in `disconnectedCallback`, and here the mount's
|
|
22
|
+
* teardown does it, so every runtime gets the same guarantee rather than each
|
|
23
|
+
* harness remembering. A scheduled poll is cancelled by moving the generation
|
|
24
|
+
* on rather than by clearing a handle - `ctx.after` has no handle to clear -
|
|
25
|
+
* so a stale timer still fires and does nothing, which is the same thing from
|
|
26
|
+
* outside: no request is made.
|
|
27
|
+
*
|
|
28
|
+
* Behaviour is what the Stencil component did, to the letter, including:
|
|
29
|
+
*
|
|
30
|
+
* - a failed read STOPS the polling. Retrying a 403 a dozen times helps
|
|
31
|
+
* nobody, and a failure is a different thing from an order with no tickets.
|
|
32
|
+
* - `loading` is only ever true before the first read finishes. Every later
|
|
33
|
+
* read, including a poll and a new order id, leaves whatever is on screen.
|
|
34
|
+
* - pointing it at another order clears the tickets without putting the
|
|
35
|
+
* loading state back, so there is a beat where the element reads as "taking
|
|
36
|
+
* longer than usual" before the new read lands. Kept, not tidied.
|
|
37
|
+
*
|
|
38
|
+
* There is no `tlError` here, and no event of any kind: nothing this element
|
|
39
|
+
* does is announced to the host. Kept as found.
|
|
40
|
+
*/
|
|
41
|
+
const define_js_1 = require("../define.js");
|
|
42
|
+
/**
|
|
43
|
+
* Gaps between polls, in milliseconds. Front-loaded because issuance usually
|
|
44
|
+
* lands in the first few seconds, then widening so a genuinely stuck order is
|
|
45
|
+
* not hammered. Sums to roughly one minute, and running out is what turns
|
|
46
|
+
* `issuing` into `delayed`.
|
|
47
|
+
*/
|
|
48
|
+
exports.POLL_DELAYS_MS = [
|
|
49
|
+
1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 12000,
|
|
50
|
+
];
|
|
51
|
+
const STRINGS = {
|
|
52
|
+
loading: 'Loading tickets…',
|
|
53
|
+
preparing: 'Preparing your tickets…',
|
|
54
|
+
delayed: 'Your tickets are taking longer than usual. They will be emailed to you as soon as they are ready.',
|
|
55
|
+
notAvailable: 'Tickets are not available',
|
|
56
|
+
loadFailed: 'Failed to load tickets',
|
|
57
|
+
};
|
|
58
|
+
exports.orderTicketsCore = (0, define_js_1.defineElement)({
|
|
59
|
+
tag: 'tl-order-tickets',
|
|
60
|
+
docs: "Every ticket in an order as a scannable card, waiting out issuance rather than reporting an empty order as empty.",
|
|
61
|
+
category: 'tickets',
|
|
62
|
+
runtimes: ['web', 'native'],
|
|
63
|
+
tokens: ['border', 'error', 'font-family', 'muted-foreground', 'primary', 'spacing-2', 'spacing-4', 'spacing-6'],
|
|
64
|
+
sdkMethods: ['orders.get'],
|
|
65
|
+
props: {
|
|
66
|
+
orderId: { type: 'string', attr: 'order-id', required: true, docs: 'The order whose tickets to show.' },
|
|
67
|
+
columns: { type: 'number', attr: 'columns', default: 2, docs: 'Columns in the grid (default 2).' },
|
|
68
|
+
},
|
|
69
|
+
events: {},
|
|
70
|
+
strings: STRINGS,
|
|
71
|
+
statuses: ['loading', 'error', 'issuing', 'delayed', 'ready'],
|
|
72
|
+
state: () => ({ tickets: [], loading: true, error: null, issuing: false }),
|
|
73
|
+
/**
|
|
74
|
+
* On screen: read, and keep reading while issuance is in flight. Off screen:
|
|
75
|
+
* stop, which is the whole reason the polling lives in the mount.
|
|
76
|
+
*/
|
|
77
|
+
mount: (ctx) => {
|
|
78
|
+
ctx.onTeardown(() => ctx.actions.stopPolling());
|
|
79
|
+
void ctx.actions.load();
|
|
80
|
+
},
|
|
81
|
+
status: ({ state }) => state.loading
|
|
82
|
+
? 'loading'
|
|
83
|
+
: state.error
|
|
84
|
+
? 'error'
|
|
85
|
+
: state.tickets.length > 0
|
|
86
|
+
? 'ready'
|
|
87
|
+
: state.issuing
|
|
88
|
+
? 'issuing'
|
|
89
|
+
: 'delayed',
|
|
90
|
+
derive: ({ state, props }) => ({
|
|
91
|
+
tickets: state.tickets,
|
|
92
|
+
columns: props.columns,
|
|
93
|
+
orderId: props.orderId,
|
|
94
|
+
}),
|
|
95
|
+
actions: (ctx) => {
|
|
96
|
+
// Neither of these is state: nothing renders from them, and the component
|
|
97
|
+
// held both as instance fields for the same reason. `generation` is how a
|
|
98
|
+
// scheduled poll is cancelled, since `ctx.after` hands back no handle.
|
|
99
|
+
let generation = 0;
|
|
100
|
+
let poll = 0;
|
|
101
|
+
const self = {
|
|
102
|
+
/**
|
|
103
|
+
* Read the order. Tickets end the waiting; no tickets and a gap left in
|
|
104
|
+
* the schedule extends it; no tickets and no gap left gives up.
|
|
105
|
+
*/
|
|
106
|
+
async load() {
|
|
107
|
+
try {
|
|
108
|
+
ctx.setState({ error: null });
|
|
109
|
+
const client = await ctx.whenClient();
|
|
110
|
+
const orders = client.getOrdersManager?.();
|
|
111
|
+
if (!orders)
|
|
112
|
+
throw new Error(ctx.strings.notAvailable);
|
|
113
|
+
const order = (await orders.get(ctx.props.orderId));
|
|
114
|
+
const tickets = order?.tickets ?? [];
|
|
115
|
+
ctx.setState({ tickets });
|
|
116
|
+
if (tickets.length > 0) {
|
|
117
|
+
// Arrived. Stop asking.
|
|
118
|
+
ctx.setState({ issuing: false });
|
|
119
|
+
self.stopPolling();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const delay = exports.POLL_DELAYS_MS[poll];
|
|
123
|
+
// `undefined` once the schedule runs out: give up waiting, and say so.
|
|
124
|
+
ctx.setState({ issuing: delay !== undefined });
|
|
125
|
+
if (delay !== undefined) {
|
|
126
|
+
poll += 1;
|
|
127
|
+
self.stopPolling();
|
|
128
|
+
const mine = generation;
|
|
129
|
+
ctx.after(delay, () => {
|
|
130
|
+
if (mine === generation)
|
|
131
|
+
void self.load();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// A failed request is a different thing from an order with no
|
|
138
|
+
// tickets, and stops the polling.
|
|
139
|
+
//
|
|
140
|
+
// Defensive rather than load bearing, as it was in the component:
|
|
141
|
+
// a poll is scheduled only on the success path, and the timer that
|
|
142
|
+
// triggered this read has already fired, so there is normally
|
|
143
|
+
// nothing left to cancel. Mutation testing says so out loud -
|
|
144
|
+
// removing this line kills no test - and it is kept because the
|
|
145
|
+
// component had it and because it is what makes the intent legible.
|
|
146
|
+
self.stopPolling();
|
|
147
|
+
ctx.setState({ issuing: false, error: err instanceof Error ? err.message : ctx.strings.loadFailed });
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
ctx.setState({ loading: false });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
/**
|
|
154
|
+
* A different order: forget this one, put the schedule back to the top
|
|
155
|
+
* and read. The loading state is deliberately NOT restored, so the
|
|
156
|
+
* element reads as "taking longer than usual" for a beat, exactly as the
|
|
157
|
+
* component did.
|
|
158
|
+
*/
|
|
159
|
+
orderIdChanged() {
|
|
160
|
+
self.stopPolling();
|
|
161
|
+
poll = 0;
|
|
162
|
+
ctx.setState({ tickets: [], issuing: false });
|
|
163
|
+
void self.load();
|
|
164
|
+
},
|
|
165
|
+
/** Cancel whatever poll is scheduled. Also what the teardown runs. */
|
|
166
|
+
stopPolling() {
|
|
167
|
+
generation += 1;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
return self;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promoCodeCore = void 0;
|
|
4
|
+
exports.isAuthError = isAuthError;
|
|
5
|
+
/**
|
|
6
|
+
* `tl-promo-code`: an input and an Apply button that redeems a presale or
|
|
7
|
+
* promo code for the current cart.
|
|
8
|
+
*
|
|
9
|
+
* Redemption unlocks gated pricing on the customer; the client re-reads the
|
|
10
|
+
* cart and publishes `cart:updated`, so `tl-cart` and the badges re-render on
|
|
11
|
+
* their own and this element subscribes to nothing. It has nothing to do when
|
|
12
|
+
* a harness mounts it either, so it declares no `mount` hook - the harness
|
|
13
|
+
* still calls `mount()` and still gets a teardown, which is the point of the
|
|
14
|
+
* hook being unconditional.
|
|
15
|
+
*
|
|
16
|
+
* The API has no un-redeem, so the applied state offers only "use a different
|
|
17
|
+
* code", which clears the input for another redemption.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour is what the Stencil component did before the split, to the letter,
|
|
20
|
+
* including the two error messages and the uppercased code in the receipt. What
|
|
21
|
+
* did not come across is the `console.error` tracing.
|
|
22
|
+
*/
|
|
23
|
+
const define_js_1 = require("../define.js");
|
|
24
|
+
const STRINGS = {
|
|
25
|
+
applying: 'Applying…',
|
|
26
|
+
codePrefix: 'Code ',
|
|
27
|
+
appliedSuffix: ' applied',
|
|
28
|
+
alreadyHeldSuffix: ' was already applied',
|
|
29
|
+
useDifferent: 'Use a different code',
|
|
30
|
+
enterCode: 'Enter a code',
|
|
31
|
+
notAvailable: 'Promo codes are not available',
|
|
32
|
+
signIn: 'Sign in to use this code',
|
|
33
|
+
rejected: 'That code could not be applied',
|
|
34
|
+
};
|
|
35
|
+
/** True for the "not signed in" family of API errors (401/403, NO_SESSION, NOT_AUTHENTICATED). */
|
|
36
|
+
function isAuthError(err) {
|
|
37
|
+
const e = err;
|
|
38
|
+
if (!e || typeof e !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
return e.status === 401 || e.status === 403 || e.code === 'NO_SESSION' || e.code === 'NOT_AUTHENTICATED';
|
|
41
|
+
}
|
|
42
|
+
exports.promoCodeCore = (0, define_js_1.defineElement)({
|
|
43
|
+
tag: 'tl-promo-code',
|
|
44
|
+
docs: 'Redeem a presale or promo code for the current cart.',
|
|
45
|
+
category: 'purchase',
|
|
46
|
+
runtimes: ['web'],
|
|
47
|
+
tokens: [
|
|
48
|
+
'background',
|
|
49
|
+
'border',
|
|
50
|
+
'error',
|
|
51
|
+
'font-family',
|
|
52
|
+
'font-family-mono',
|
|
53
|
+
'foreground',
|
|
54
|
+
'muted',
|
|
55
|
+
'muted-foreground',
|
|
56
|
+
'primary',
|
|
57
|
+
'primary-foreground',
|
|
58
|
+
'radius-md',
|
|
59
|
+
'spacing-2',
|
|
60
|
+
'spacing-3',
|
|
61
|
+
'spacing-4',
|
|
62
|
+
'success',
|
|
63
|
+
],
|
|
64
|
+
sdkMethods: ['cart.get', 'presale.redeem'],
|
|
65
|
+
props: {
|
|
66
|
+
placeholder: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
attr: 'placeholder',
|
|
69
|
+
default: 'Promo or presale code',
|
|
70
|
+
docs: 'Input placeholder.',
|
|
71
|
+
},
|
|
72
|
+
buttonLabel: { type: 'string', attr: 'button-label', default: 'Apply', docs: 'Apply button label.' },
|
|
73
|
+
},
|
|
74
|
+
events: {
|
|
75
|
+
tlPromoApplied: { detail: 'TlPromoApplied', docs: '' },
|
|
76
|
+
tlError: { detail: '{ message: string; }', docs: '' },
|
|
77
|
+
},
|
|
78
|
+
strings: STRINGS,
|
|
79
|
+
statuses: ['idle', 'applied'],
|
|
80
|
+
state: () => ({ code: '', applying: false, applied: null, error: null }),
|
|
81
|
+
status: ({ state }) => (state.applied ? 'applied' : 'idle'),
|
|
82
|
+
derive: ({ state, props, strings }) => ({
|
|
83
|
+
actionLabel: state.applying ? strings.applying : props.buttonLabel,
|
|
84
|
+
disabled: state.applying,
|
|
85
|
+
appliedCode: state.applied?.code ?? null,
|
|
86
|
+
appliedSuffix: state.applied?.alreadyHeld ? strings.alreadyHeldSuffix : strings.appliedSuffix,
|
|
87
|
+
}),
|
|
88
|
+
actions: (ctx) => ({
|
|
89
|
+
/** The buyer typed. Typing clears whatever the last attempt complained about. */
|
|
90
|
+
setCode(code) {
|
|
91
|
+
ctx.setState({ code, error: null });
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* Redeem what is in the box.
|
|
95
|
+
*
|
|
96
|
+
* The client refreshes the cart on redeem, so the cart is read back only
|
|
97
|
+
* to put it in the payload; a read that fails is not a failed redemption
|
|
98
|
+
* and does not become one.
|
|
99
|
+
*/
|
|
100
|
+
async apply() {
|
|
101
|
+
const code = ctx.state.code.trim();
|
|
102
|
+
if (!code) {
|
|
103
|
+
ctx.setState({ error: ctx.strings.enterCode });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
ctx.setState({ applying: true, error: null });
|
|
107
|
+
try {
|
|
108
|
+
const client = await ctx.whenClient();
|
|
109
|
+
const presale = client.getPresaleManager?.();
|
|
110
|
+
if (!presale)
|
|
111
|
+
throw new Error(ctx.strings.notAvailable);
|
|
112
|
+
const result = await presale.redeem(code);
|
|
113
|
+
const cart = (await client.getCartManager?.()?.get?.().catch(() => null)) ?? null;
|
|
114
|
+
const applied = { code: code.toUpperCase(), ...result, cart };
|
|
115
|
+
ctx.setState({ applied, code: '' });
|
|
116
|
+
ctx.emit('tlPromoApplied', applied);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = isAuthError(err)
|
|
120
|
+
? ctx.strings.signIn
|
|
121
|
+
: err instanceof Error && err.message
|
|
122
|
+
? err.message
|
|
123
|
+
: ctx.strings.rejected;
|
|
124
|
+
ctx.setState({ error: message });
|
|
125
|
+
ctx.emit('tlError', { message });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
ctx.setState({ applying: false });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
/** Back to the input, for a second code. There is no un-redeem to offer. */
|
|
132
|
+
reset() {
|
|
133
|
+
ctx.setState({ applied: null, error: null });
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
@@ -95,6 +95,36 @@ exports.ticketSelectorCore = (0, define_js_1.defineElement)({
|
|
|
95
95
|
addSuccess: false,
|
|
96
96
|
feeQuote: null,
|
|
97
97
|
}),
|
|
98
|
+
/**
|
|
99
|
+
* On screen: wait for the host's client, then read the ticket types. The
|
|
100
|
+
* harness used to do this (it awaited `whenSDK()` and called `load()`
|
|
101
|
+
* itself); the core does it now, so every runtime loads at the same moment
|
|
102
|
+
* and in the same order (TKT-197).
|
|
103
|
+
*
|
|
104
|
+
* There is nothing to undo, so nothing is returned. The guards are what make
|
|
105
|
+
* a remount safe: `active()` is false once this mount has been torn down, so
|
|
106
|
+
* an element that was removed while the client was still coming up neither
|
|
107
|
+
* loads nor writes an error into a view nobody is rendering.
|
|
108
|
+
*/
|
|
109
|
+
mount: (ctx) => {
|
|
110
|
+
// Nothing to read until the host has said which occurrence is on sale.
|
|
111
|
+
// Until then the element stays in `loading`, which is what it did before.
|
|
112
|
+
if (!ctx.props.eventId || !ctx.props.occurrenceId)
|
|
113
|
+
return;
|
|
114
|
+
void (async () => {
|
|
115
|
+
try {
|
|
116
|
+
await ctx.whenClient();
|
|
117
|
+
if (!ctx.active())
|
|
118
|
+
return;
|
|
119
|
+
await ctx.actions.load();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (!ctx.active())
|
|
123
|
+
return;
|
|
124
|
+
ctx.actions.connectFailed(err instanceof Error ? err.message : ctx.strings.noClient);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
},
|
|
98
128
|
status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
|
|
99
129
|
derive: (ctx) => {
|
|
100
130
|
const { state, strings, props, formatMoney } = ctx;
|