@session-services/web-elements 0.1.20 → 0.1.22

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.
Files changed (32) hide show
  1. package/dist/cjs/{index-C6e98Atu.js → index-BYBgrATL.js} +1 -0
  2. package/dist/cjs/loader.cjs.js +2 -2
  3. package/dist/cjs/session-services-elements.cjs.js +2 -2
  4. package/dist/cjs/session-services-formatted-text.cjs.entry.js +181 -165
  5. package/dist/cjs/session-services-ticketing.cjs.entry.js +24370 -19296
  6. package/dist/collection/components/session-services-formatted-text/session-services-formatted-text.js +33 -34
  7. package/dist/collection/components/session-services-ticketing/session-services-ticketing.css +429 -340
  8. package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +1090 -269
  9. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +270 -0
  10. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +368 -0
  11. package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js +264 -0
  12. package/dist/components/session-services-formatted-text.js +180 -164
  13. package/dist/components/session-services-ticketing.js +24458 -19366
  14. package/dist/esm/{index-CeXOR4aK.js → index-CNOpF6tk.js} +1 -1
  15. package/dist/esm/loader.js +3 -3
  16. package/dist/esm/session-services-elements.js +3 -3
  17. package/dist/esm/session-services-formatted-text.entry.js +181 -165
  18. package/dist/esm/session-services-ticketing.entry.js +24378 -19304
  19. package/dist/session-services-elements/{p-a2404c46.entry.js → p-703444ed.entry.js} +1 -1
  20. package/dist/session-services-elements/p-94173d1b.entry.js +1 -0
  21. package/dist/session-services-elements/p-CNOpF6tk.js +2 -0
  22. package/dist/session-services-elements/session-services-elements.esm.js +1 -1
  23. package/dist/sst-env.d.ts +1 -0
  24. package/dist/types/components/session-services-formatted-text/session-services-formatted-text.d.ts +3 -0
  25. package/dist/types/components/session-services-ticketing/session-services-ticketing.d.ts +117 -3
  26. package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.d.ts +196 -0
  27. package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.test.d.ts +1 -0
  28. package/dist/types/components/session-services-ticketing/session-services-ticketing.test.d.ts +1 -0
  29. package/dist/types/components.d.ts +18 -0
  30. package/package.json +8 -7
  31. package/dist/session-services-elements/p-3bb6ceef.entry.js +0 -1
  32. package/dist/session-services-elements/p-CeXOR4aK.js +0 -2
@@ -1,28 +1,116 @@
1
1
  import { createClient } from "@session-services/sdk";
2
2
  import { createAnalytics, formatPrice } from "@session-services/sdk/analytics";
3
- import {
3
+ import { formatNaiveDateTime } from "@session-services/utils/naive-datetime";
4
+ import { normalizeCouponCode } from "@session-services/utils/ticket-quote";
5
+ import { isSectionSoldOut } from "@session-services/utils/ticket-availability";
6
+ import { Host,
4
7
  // biome-ignore lint/correctness/noUnusedImports: h is required by Stencil JSX transform
5
- h, Host, } from "@stencil/core";
8
+ h, } from "@stencil/core";
9
+ import { admissionPriceSavings, discountCouponFromSearch, getAdmissionDisplayState, getAdmissionPriceDisplay, getTicketingState, parseSelectionKey, previewQuoteItems, priceTotal, quoteItemsBySelection, selectionKey, topLevelTicketItems, visibleAdmissions, } from "./session-services-ticketing.logic";
10
+ const quoteTotal = (quote) => quote.price?.total ?? 0;
11
+ const QUOTE_DEBOUNCE_MS = 150;
12
+ function objectValue(value, key) {
13
+ if (!value || typeof value !== "object")
14
+ return undefined;
15
+ return value[key];
16
+ }
17
+ function errorMessage(err, fallback) {
18
+ return err instanceof Error ? err.message : fallback;
19
+ }
20
+ function errorCode(err) {
21
+ const code = objectValue(err, "code");
22
+ return typeof code === "string" ? code : undefined;
23
+ }
24
+ function isPasscodeAccessError(err) {
25
+ const message = errorMessage(err, "");
26
+ if (/incorrect passcode/i.test(message))
27
+ return true;
28
+ return errorCode(err) === "UNAUTHORIZED" && /passcode/i.test(message);
29
+ }
30
+ function parseJson(value) {
31
+ try {
32
+ return JSON.parse(value);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function hasAnalyticsIds(event) {
39
+ return Boolean(event.teamId && event.promoterId);
40
+ }
41
+ function eventVenueName(event) {
42
+ return [event.location?.venue?.name, event.location?.name].find(Boolean);
43
+ }
44
+ function optionalId(id) {
45
+ return id || undefined;
46
+ }
47
+ function admissionAriaLabel(admission, section, status) {
48
+ if (section)
49
+ return `${admission.name} in ${section.name} - ${status}`;
50
+ return `${admission.name} - ${status}`;
51
+ }
52
+ function hasQuoteItems(quote) {
53
+ return Boolean(quote?.items?.length);
54
+ }
55
+ function quoteProductIdentity(item) {
56
+ return {
57
+ product_id: item.admissionId,
58
+ sku: item.admissionId,
59
+ quantity: item.quantity,
60
+ admission_id: item.admissionId,
61
+ section_id: item.sectionId,
62
+ };
63
+ }
64
+ function quoteProductContext({ event, admission, section, }) {
65
+ return {
66
+ name: admission?.name,
67
+ category: event?.name,
68
+ variant: section?.name,
69
+ };
70
+ }
71
+ function quoteProductPrice(item) {
72
+ return {
73
+ price: priceTotal(item.price) / 100,
74
+ price_base: item.price?.base,
75
+ price_booking_fee: item.price?.booking,
76
+ };
77
+ }
6
78
  const CalendarIcon = () => (h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, h("path", { d: "M8 2v4" }), h("path", { d: "M16 2v4" }), h("rect", { width: "18", height: "18", x: "3", y: "4", rx: "2" }), h("path", { d: "M3 10h18" })));
7
79
  const LocationIcon = () => (h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, h("path", { d: "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" }), h("circle", { cx: "12", cy: "10", r: "3" })));
8
80
  const InfoIcon = () => (h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("path", { d: "M12 16v-4" }), h("path", { d: "M12 8h.01" })));
9
81
  const CloseIcon = () => (h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, h("path", { d: "M18 6 6 18" }), h("path", { d: "M6 6l12 12" })));
82
+ const LoaderCircleIcon = () => (h("svg", { class: "checkout-spinner", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, h("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })));
10
83
  export class SessionsTicketing {
84
+ host;
11
85
  tenantId;
12
86
  eventId;
13
87
  apiUrl = "https://api.session.services";
14
88
  locale = "en-US";
15
89
  returnUrl;
90
+ couponCode;
91
+ initialAdmissionPrices;
92
+ initialPreviewQuote;
93
+ passcode;
94
+ stickyCheckout = false;
16
95
  theme = "system";
17
96
  enableAnalytics = true;
18
97
  debugAnalytics = false;
19
98
  event = null;
20
99
  selectedItems = new Map();
21
100
  quote = null;
101
+ previewQuote = null;
22
102
  loading = true;
23
103
  error = null;
24
104
  quoteLoading = false;
105
+ previewLoading = false;
25
106
  quoteError = null;
107
+ couponError = null;
108
+ couponInput = "";
109
+ appliedCouponCode = "";
110
+ passcodeInput = "";
111
+ activePasscode = "";
112
+ passcodeDenied = false;
113
+ selectionPrompt = false;
26
114
  checkoutLoading = false;
27
115
  currentTheme = "light";
28
116
  openModalAdmissionId = null;
@@ -33,16 +121,28 @@ export class SessionsTicketing {
33
121
  analytics;
34
122
  mediaQueryList;
35
123
  requestId = 0;
124
+ previewRequestId = 0;
125
+ quoteTimer;
126
+ promptTimer;
36
127
  async componentWillLoad() {
128
+ if (typeof window !== "undefined") {
129
+ window.addEventListener("popstate", this.handleDiscountQueryChange);
130
+ }
37
131
  await this.initializeComponent();
38
132
  }
39
133
  async propsChanged() {
40
134
  await this.initializeComponent();
41
135
  }
136
+ async couponCodeChanged() {
137
+ await this.setInitialCoupon();
138
+ }
42
139
  themeChanged() {
43
140
  this.setupTheme();
44
141
  }
45
142
  disconnectedCallback() {
143
+ if (typeof window !== "undefined") {
144
+ window.removeEventListener("popstate", this.handleDiscountQueryChange);
145
+ }
46
146
  if (this.mediaQueryList) {
47
147
  this.mediaQueryList.removeEventListener("change", this.handleSystemThemeChange);
48
148
  }
@@ -50,12 +150,27 @@ export class SessionsTicketing {
50
150
  if (this.openModalAdmissionId) {
51
151
  document.removeEventListener("keydown", this.handleModalKeyDown);
52
152
  }
153
+ this.clearQuoteTimer();
154
+ this.clearPromptTimer();
53
155
  }
156
+ handleDiscountQueryChange = () => {
157
+ if (normalizeCouponCode(this.couponCode))
158
+ return;
159
+ this.setInitialCoupon().catch(() => undefined);
160
+ };
54
161
  async initializeComponent() {
55
162
  this.error = null;
56
163
  this.event = null;
57
- this.selectedItems.clear();
164
+ this.selectedItems = new Map();
58
165
  this.quote = null;
166
+ this.previewQuote = null;
167
+ this.quoteError = null;
168
+ this.couponError = null;
169
+ this.couponInput = this.initialCouponCode();
170
+ this.appliedCouponCode = this.couponInput;
171
+ this.activePasscode = this.passcode || "";
172
+ this.passcodeInput = this.activePasscode;
173
+ this.passcodeDenied = false;
59
174
  if (!this.tenantId || !this.eventId) {
60
175
  this.error = "Missing required configuration: tenantId and eventId";
61
176
  this.loading = false;
@@ -68,6 +183,62 @@ export class SessionsTicketing {
68
183
  this.setupTheme();
69
184
  await this.loadEventData();
70
185
  }
186
+ async setInitialCoupon() {
187
+ const coupon = this.initialCouponCode();
188
+ this.couponInput = coupon;
189
+ this.quote = null;
190
+ this.previewQuote = this.activeInitialPreviewQuote(coupon);
191
+ this.quoteError = null;
192
+ this.couponError = null;
193
+ if (!coupon) {
194
+ this.appliedCouponCode = "";
195
+ await this.updateQuote();
196
+ return;
197
+ }
198
+ if (this.selectedItems.size > 0) {
199
+ await this.applyCartCoupon(coupon);
200
+ return;
201
+ }
202
+ if (this.previewQuote)
203
+ return;
204
+ await this.applyPreviewCoupon(coupon);
205
+ }
206
+ initialCouponCode() {
207
+ const prop = normalizeCouponCode(this.couponCode);
208
+ if (prop)
209
+ return prop;
210
+ if (typeof window === "undefined")
211
+ return "";
212
+ return normalizeCouponCode(discountCouponFromSearch(window.location.search));
213
+ }
214
+ parsedInitialPreviewQuote() {
215
+ if (!this.initialPreviewQuote)
216
+ return null;
217
+ if (typeof this.initialPreviewQuote !== "string")
218
+ return this.initialPreviewQuote;
219
+ return parseJson(this.initialPreviewQuote);
220
+ }
221
+ parsedInitialAdmissionPrices() {
222
+ if (!this.initialAdmissionPrices)
223
+ return undefined;
224
+ if (typeof this.initialAdmissionPrices !== "string")
225
+ return this.initialAdmissionPrices;
226
+ return (parseJson(this.initialAdmissionPrices) ?? undefined);
227
+ }
228
+ activeInitialPreviewQuote(coupon = this.appliedCouponCode) {
229
+ if (!coupon)
230
+ return null;
231
+ if (coupon !== this.initialCouponCode())
232
+ return null;
233
+ return this.parsedInitialPreviewQuote();
234
+ }
235
+ activeInitialAdmissionPrices() {
236
+ if (!this.appliedCouponCode)
237
+ return undefined;
238
+ if (this.appliedCouponCode !== this.initialCouponCode())
239
+ return undefined;
240
+ return this.parsedInitialAdmissionPrices();
241
+ }
71
242
  setupTheme() {
72
243
  if (this.mediaQueryList) {
73
244
  this.mediaQueryList.removeEventListener("change", this.handleSystemThemeChange);
@@ -87,140 +258,194 @@ export class SessionsTicketing {
87
258
  }
88
259
  };
89
260
  trackEvent(eventType, properties = {}) {
90
- if (!this.analytics || !this.enableAnalytics)
91
- return;
92
- // Ensure required fields exist before tracking
93
- if (!this.event?.teamId || !this.event?.promoterId)
261
+ const context = this.analyticsContext();
262
+ if (!context)
94
263
  return;
95
- this.analytics.track(eventType, {
264
+ context.analytics.track(eventType, {
96
265
  event_id: this.eventId,
97
- team_id: this.event.teamId,
98
- promoter_id: this.event.promoterId,
99
- event_name: this.event.name,
100
- event_slug: this.event.slug,
101
- currency: this.event.currency,
266
+ team_id: context.event.teamId,
267
+ promoter_id: context.event.promoterId,
268
+ event_name: context.event.name,
269
+ event_slug: context.event.slug,
270
+ currency: context.event.currency,
102
271
  ...properties,
103
272
  });
104
273
  }
274
+ analyticsContext() {
275
+ const analytics = this.activeAnalytics();
276
+ const event = this.analyticsEvent();
277
+ if (!analytics || !event)
278
+ return undefined;
279
+ return { analytics, event };
280
+ }
281
+ activeAnalytics() {
282
+ if (!this.enableAnalytics)
283
+ return undefined;
284
+ return this.analytics;
285
+ }
286
+ analyticsEvent() {
287
+ const event = this.event;
288
+ if (!event)
289
+ return undefined;
290
+ return hasAnalyticsIds(event) ? event : undefined;
291
+ }
105
292
  getLowestPrice() {
106
- if (!this.event?.admissions.length)
293
+ const event = this.event;
294
+ if (!event?.admissions.length)
107
295
  return 0;
108
- return Math.min(...this.event.admissions.map((adm) => (adm.price?.base || 0) + (adm.price?.booking || 0)));
296
+ return Math.min(...event.admissions.map((adm) => priceTotal(adm.price)));
297
+ }
298
+ getAdmissionListProducts(event) {
299
+ return event.admissions.map((adm, index) => ({
300
+ product_id: adm.id,
301
+ sku: adm.id,
302
+ name: adm.name,
303
+ category: event.name,
304
+ price: priceTotal(adm.price) / 100,
305
+ quantity: 1,
306
+ position: index + 1,
307
+ admission_id: adm.id,
308
+ section_id: adm.sectionId,
309
+ price_base: adm.price?.base,
310
+ price_booking_fee: adm.price?.booking,
311
+ available: adm.available,
312
+ capacity: adm.capacity,
313
+ }));
109
314
  }
110
315
  async loadEventData() {
111
316
  try {
112
317
  this.loading = true;
113
318
  this.error = null;
114
- const response = await this.client.event.get({ key: this.eventId });
319
+ this.passcodeDenied = false;
320
+ const response = await this.client.event.get({
321
+ key: this.eventId,
322
+ passcode: this.activePasscode || undefined,
323
+ });
115
324
  this.event = response.event;
116
- if (this.enableAnalytics && !this.analytics && this.event?.promoterId) {
117
- this.analytics = createAnalytics({
118
- client: this.client,
119
- debug: this.debugAnalytics,
120
- });
121
- this.analytics.page("Ticketing Widget", {
122
- event_id: this.eventId,
123
- team_id: this.event.teamId,
124
- promoter_id: this.event.promoterId,
125
- event_name: this.event.name,
126
- event_slug: this.event.slug,
127
- });
128
- }
129
- if (this.event) {
130
- this.trackEvent("Product Viewed", {
131
- product_id: this.eventId,
132
- sku: this.eventId,
133
- name: this.event.name,
134
- category: "Event",
135
- value: this.getLowestPrice() / 100,
136
- price: this.getLowestPrice() / 100,
137
- event_date: this.event.entryStartsAt,
138
- venue_name: this.event.location?.venue?.name || this.event.location?.name,
139
- venue_city: this.event.location?.city,
140
- });
141
- this.trackEvent("Product List Viewed", {
142
- list_id: this.eventId,
143
- category: "Admissions",
144
- products: this.event.admissions.map((adm, index) => ({
145
- product_id: adm.id,
146
- sku: adm.id,
147
- name: adm.name,
148
- category: this.event?.name,
149
- price: ((adm.price?.base || 0) + (adm.price?.booking || 0)) / 100,
150
- quantity: 1,
151
- position: index + 1,
152
- admission_id: adm.id,
153
- section_id: adm.sectionId,
154
- price_base: adm.price?.base,
155
- price_booking_fee: adm.price?.booking,
156
- available: adm.available,
157
- capacity: adm.capacity,
158
- })),
159
- });
160
- }
325
+ this.trackLoadedEvent(response.event);
326
+ await this.updatePreviewQuote();
161
327
  }
162
328
  catch (err) {
163
- this.error =
164
- err instanceof Error ? err.message : "Failed to load event data";
329
+ this.passcodeDenied = isPasscodeAccessError(err);
330
+ this.error = errorMessage(err, "Failed to load event data");
165
331
  this.errorOccurred.emit({ error: this.error });
166
332
  }
167
333
  finally {
168
334
  this.loading = false;
169
335
  }
170
336
  }
337
+ trackLoadedEvent(event) {
338
+ this.setupAnalytics(event);
339
+ this.trackProductViewed(event);
340
+ this.trackEvent("Product List Viewed", {
341
+ list_id: this.eventId,
342
+ category: "Admissions",
343
+ products: this.getAdmissionListProducts(event),
344
+ });
345
+ }
346
+ setupAnalytics(event) {
347
+ if (!this.enableAnalytics || this.analytics || !event.promoterId)
348
+ return;
349
+ this.analytics = createAnalytics({
350
+ client: this.client,
351
+ debug: this.debugAnalytics,
352
+ });
353
+ this.analytics.page("Ticketing Widget", {
354
+ event_id: this.eventId,
355
+ team_id: event.teamId,
356
+ promoter_id: event.promoterId,
357
+ event_name: event.name,
358
+ event_slug: event.slug,
359
+ });
360
+ }
361
+ trackProductViewed(event) {
362
+ const lowestPrice = this.getLowestPrice();
363
+ this.trackEvent("Product Viewed", {
364
+ product_id: this.eventId,
365
+ sku: this.eventId,
366
+ name: event.name,
367
+ category: "Event",
368
+ value: lowestPrice / 100,
369
+ price: lowestPrice / 100,
370
+ event_date: event.entryStartsAt,
371
+ venue_name: eventVenueName(event),
372
+ venue_city: event.location?.city,
373
+ });
374
+ }
171
375
  async updateQuantity(admissionId, sectionId, delta) {
172
- const key = `${admissionId}::${sectionId || "none"}`;
173
- const currentQuantity = this.selectedItems.get(key) || 0;
174
- const newQuantity = Math.max(0, currentQuantity + delta);
175
- if (newQuantity === 0) {
176
- this.selectedItems.delete(key);
177
- }
178
- else {
179
- this.selectedItems.set(key, newQuantity);
180
- }
181
- this.selectedItems = new Map(this.selectedItems);
376
+ const key = selectionKey(admissionId, sectionId);
377
+ await this.setQuantity(admissionId, sectionId, (this.selectedItems.get(key) || 0) + delta);
378
+ }
379
+ async setQuantity(admissionId, sectionId, quantity) {
380
+ const key = selectionKey(admissionId, sectionId);
182
381
  const admission = this.event?.admissions.find((a) => a.id === admissionId);
382
+ if (!admission)
383
+ return;
384
+ const previous = this.selectedItems.get(key) || 0;
385
+ const next = Math.max(0, Math.min(quantity, this.getMaxQuantity(admission, sectionId)));
386
+ if (previous === next)
387
+ return;
388
+ this.selectionPrompt = false;
389
+ this.setSelectedQuantity(key, next);
183
390
  const section = sectionId
184
391
  ? this.event?.sections.find((s) => s.id === sectionId)
185
392
  : null;
186
- if (delta > 0 && admission) {
187
- this.trackEvent("Product Added", {
188
- product_id: admissionId,
189
- sku: admissionId,
190
- name: admission.name,
191
- category: this.event?.name,
192
- variant: section?.name,
193
- price: ((admission.price?.base || 0) + (admission.price?.booking || 0)) /
194
- 100,
195
- quantity: Math.abs(delta),
196
- admission_id: admissionId,
197
- section_id: sectionId || undefined,
198
- section_name: section?.name,
199
- price_base: admission.price?.base,
200
- price_booking_fee: admission.price?.booking,
201
- });
393
+ this.trackQuantityChange({
394
+ admission,
395
+ section,
396
+ sectionId,
397
+ delta: next - previous,
398
+ });
399
+ this.scheduleQuoteUpdate();
400
+ }
401
+ setSelectedQuantity(key, quantity) {
402
+ if (quantity === 0) {
403
+ this.selectedItems.delete(key);
202
404
  }
203
- else if (delta < 0 && admission) {
204
- this.trackEvent("Product Removed", {
205
- product_id: admissionId,
206
- sku: admissionId,
207
- name: admission.name,
208
- category: this.event?.name,
209
- variant: section?.name,
210
- price: ((admission.price?.base || 0) + (admission.price?.booking || 0)) /
211
- 100,
212
- quantity: Math.abs(delta),
213
- admission_id: admissionId,
214
- section_id: sectionId || undefined,
215
- section_name: section?.name,
216
- });
405
+ else {
406
+ this.selectedItems.set(key, quantity);
217
407
  }
218
- await this.updateQuote();
408
+ this.selectedItems = new Map(this.selectedItems);
409
+ }
410
+ trackQuantityChange(change) {
411
+ if (change.delta > 0) {
412
+ this.trackProductAdded(change);
413
+ return;
414
+ }
415
+ if (change.delta < 0)
416
+ this.trackProductRemoved(change);
417
+ }
418
+ trackProductAdded(change) {
419
+ this.trackEvent("Product Added", {
420
+ ...this.getQuantityAnalytics(change),
421
+ price_base: change.admission.price?.base,
422
+ price_booking_fee: change.admission.price?.booking,
423
+ });
424
+ }
425
+ trackProductRemoved(change) {
426
+ this.trackEvent("Product Removed", this.getQuantityAnalytics(change));
427
+ }
428
+ getQuantityAnalytics(change) {
429
+ return {
430
+ product_id: change.admission.id,
431
+ sku: change.admission.id,
432
+ name: change.admission.name,
433
+ category: this.event?.name,
434
+ variant: change.section?.name,
435
+ price: priceTotal(change.admission.price) / 100,
436
+ quantity: Math.abs(change.delta),
437
+ admission_id: change.admission.id,
438
+ section_id: optionalId(change.sectionId),
439
+ section_name: change.section?.name,
440
+ };
219
441
  }
220
442
  async updateQuote() {
443
+ this.clearQuoteTimer();
221
444
  if (this.selectedItems.size === 0) {
222
445
  this.quote = null;
223
446
  this.quoteError = null;
447
+ this.quoteLoading = false;
448
+ await this.updatePreviewQuote();
224
449
  return;
225
450
  }
226
451
  this.requestId++;
@@ -228,126 +453,422 @@ export class SessionsTicketing {
228
453
  try {
229
454
  this.quoteLoading = true;
230
455
  this.quoteError = null;
231
- const items = [];
232
- this.selectedItems.forEach((quantity, key) => {
233
- const [admissionId, sectionId] = key.split("::");
234
- items.push({
235
- quantity,
236
- admissionId,
237
- sectionId: sectionId !== "none" ? sectionId : undefined,
238
- });
239
- });
240
- const response = await this.client.order.quote({
241
- eventId: this.eventId,
242
- items,
243
- });
456
+ const response = await this.fetchCartQuote(this.appliedCouponCode);
457
+ this.applyQuote(response.quote, currentId);
458
+ }
459
+ catch (_err) {
460
+ this.handleQuoteError(currentId);
461
+ }
462
+ finally {
244
463
  if (currentId === this.requestId) {
245
- this.quote = response.quote;
246
- this.cartUpdate.emit(this.quote);
247
- this.trackEvent("Cart Viewed", {
248
- products: this.quote.items.map((item) => {
249
- const admission = this.event?.admissions.find((a) => a.id === item.admissionId);
250
- const section = item.sectionId
251
- ? this.event?.sections.find((s) => s.id === item.sectionId)
252
- : null;
253
- return {
254
- product_id: item.admissionId,
255
- sku: item.admissionId,
256
- name: admission?.name,
257
- category: this.event?.name,
258
- variant: section?.name,
259
- price: ((item.price?.base || 0) + (item.price?.booking || 0)) / 100,
260
- quantity: item.quantity,
261
- admission_id: item.admissionId,
262
- section_id: item.sectionId,
263
- price_base: item.price?.base,
264
- price_booking_fee: item.price?.booking,
265
- };
266
- }),
267
- value: (this.quote.price?.total || 0) / 100,
268
- currency: this.event?.currency,
269
- });
464
+ this.quoteLoading = false;
270
465
  }
271
466
  }
467
+ }
468
+ scheduleQuoteUpdate() {
469
+ this.clearQuoteTimer();
470
+ if (this.selectedItems.size === 0) {
471
+ this.updateQuote().catch(() => undefined);
472
+ return;
473
+ }
474
+ this.quoteLoading = true;
475
+ this.quoteTimer = setTimeout(() => {
476
+ this.quoteTimer = undefined;
477
+ this.updateQuote().catch(() => undefined);
478
+ }, QUOTE_DEBOUNCE_MS);
479
+ }
480
+ clearQuoteTimer() {
481
+ if (!this.quoteTimer)
482
+ return;
483
+ clearTimeout(this.quoteTimer);
484
+ this.quoteTimer = undefined;
485
+ }
486
+ clearPromptTimer() {
487
+ if (!this.promptTimer)
488
+ return;
489
+ clearTimeout(this.promptTimer);
490
+ this.promptTimer = undefined;
491
+ }
492
+ promptTicketSelection() {
493
+ this.selectionPrompt = true;
494
+ this.clearPromptTimer();
495
+ this.promptTimer = setTimeout(() => {
496
+ this.selectionPrompt = false;
497
+ this.promptTimer = undefined;
498
+ }, 800);
499
+ }
500
+ getQuoteRequestItems() {
501
+ return Array.from(this.selectedItems, ([key, quantity]) => {
502
+ const item = parseSelectionKey(key);
503
+ return {
504
+ quantity,
505
+ admissionId: item.admissionId,
506
+ sectionId: item.sectionId,
507
+ };
508
+ });
509
+ }
510
+ fetchCartQuote(couponCode) {
511
+ return this.client.order.quote({
512
+ eventId: this.eventId,
513
+ items: this.getQuoteRequestItems(),
514
+ couponCode: couponCode || undefined,
515
+ });
516
+ }
517
+ applyQuote(quote, requestId) {
518
+ if (requestId !== this.requestId)
519
+ return;
520
+ this.quote = quote;
521
+ this.quoteError = null;
522
+ this.cartUpdate.emit(this.quote);
523
+ this.trackEvent("Cart Viewed", {
524
+ products: this.getCartProducts(),
525
+ value: quoteTotal(this.quote) / 100,
526
+ currency: this.event?.currency,
527
+ });
528
+ }
529
+ handleQuoteError(requestId) {
530
+ if (requestId !== this.requestId)
531
+ return;
532
+ this.quote = null;
533
+ this.quoteError = "Failed to update cart. Please try again.";
534
+ this.errorOccurred.emit({ error: this.quoteError });
535
+ }
536
+ async updatePreviewQuote() {
537
+ const event = this.event;
538
+ if (!event || !this.appliedCouponCode) {
539
+ this.previewRequestId++;
540
+ this.previewLoading = false;
541
+ this.previewQuote = null;
542
+ return;
543
+ }
544
+ const initialQuote = this.activeInitialPreviewQuote();
545
+ if (initialQuote) {
546
+ this.previewRequestId++;
547
+ this.previewLoading = false;
548
+ this.previewQuote = initialQuote;
549
+ return;
550
+ }
551
+ const items = previewQuoteItems(event);
552
+ if (items.length === 0) {
553
+ this.previewRequestId++;
554
+ this.previewLoading = false;
555
+ this.previewQuote = null;
556
+ return;
557
+ }
558
+ this.previewRequestId++;
559
+ const currentId = this.previewRequestId;
560
+ try {
561
+ this.previewLoading = true;
562
+ const response = await this.fetchPreviewQuote(this.appliedCouponCode);
563
+ if (currentId === this.previewRequestId) {
564
+ this.previewQuote = response.quote;
565
+ }
566
+ }
567
+ catch (_err) {
568
+ if (currentId === this.previewRequestId) {
569
+ this.previewQuote = null;
570
+ this.appliedCouponCode = "";
571
+ this.setCouponError();
572
+ }
573
+ }
574
+ finally {
575
+ if (currentId === this.previewRequestId) {
576
+ this.previewLoading = false;
577
+ }
578
+ }
579
+ }
580
+ fetchPreviewQuote(couponCode) {
581
+ const event = this.event;
582
+ if (!event)
583
+ throw new Error("Missing event");
584
+ return this.client.order.quote({
585
+ eventId: event.id,
586
+ items: previewQuoteItems(event),
587
+ couponCode,
588
+ });
589
+ }
590
+ handleCouponInput = (e) => {
591
+ if (!(e.target instanceof HTMLInputElement))
592
+ return;
593
+ this.couponInput = e.target.value.toUpperCase();
594
+ };
595
+ handleCouponKeyDown = (e) => {
596
+ if (e.key !== "Enter")
597
+ return;
598
+ e.preventDefault();
599
+ this.applyCoupon().catch(() => undefined);
600
+ };
601
+ async applyCoupon() {
602
+ const coupon = normalizeCouponCode(this.couponInput);
603
+ this.couponInput = coupon;
604
+ this.quoteError = null;
605
+ this.couponError = null;
606
+ this.trackCouponEntered(coupon);
607
+ if (this.selectedItems.size > 0) {
608
+ await this.applyCartCoupon(coupon);
609
+ return;
610
+ }
611
+ await this.applyPreviewCoupon(coupon);
612
+ }
613
+ async removeCoupon() {
614
+ this.couponError = null;
615
+ this.quoteError = null;
616
+ if (this.selectedItems.size > 0) {
617
+ await this.removeCartCoupon();
618
+ return;
619
+ }
620
+ this.couponInput = "";
621
+ const coupon = this.appliedCouponCode;
622
+ this.appliedCouponCode = "";
623
+ this.previewRequestId++;
624
+ this.previewLoading = false;
625
+ this.previewQuote = null;
626
+ this.trackCouponRemoved(coupon, null);
627
+ }
628
+ async applyCartCoupon(coupon) {
629
+ this.requestId++;
630
+ const currentId = this.requestId;
631
+ try {
632
+ this.quoteLoading = true;
633
+ const response = await this.fetchCartQuote(coupon);
634
+ if (currentId !== this.requestId)
635
+ return;
636
+ this.appliedCouponCode = coupon;
637
+ this.previewQuote = null;
638
+ this.applyQuote(response.quote, currentId);
639
+ this.trackCouponApplied(coupon, response.quote);
640
+ }
272
641
  catch (_err) {
642
+ this.handleCouponError(currentId);
643
+ this.trackCouponDenied(coupon);
644
+ }
645
+ finally {
273
646
  if (currentId === this.requestId) {
274
- this.quoteError = "Failed to update cart. Please try again.";
275
- this.errorOccurred.emit({ error: this.quoteError });
647
+ this.quoteLoading = false;
648
+ }
649
+ }
650
+ }
651
+ async applyPreviewCoupon(coupon) {
652
+ const event = this.event;
653
+ if (!event)
654
+ return;
655
+ const initialQuote = this.activeInitialPreviewQuote(coupon);
656
+ if (initialQuote) {
657
+ this.appliedCouponCode = coupon;
658
+ this.previewQuote = initialQuote;
659
+ this.trackCouponApplied(coupon, initialQuote);
660
+ return;
661
+ }
662
+ if (previewQuoteItems(event).length === 0) {
663
+ this.appliedCouponCode = coupon;
664
+ this.previewQuote = null;
665
+ return;
666
+ }
667
+ this.previewRequestId++;
668
+ const currentId = this.previewRequestId;
669
+ try {
670
+ this.previewLoading = true;
671
+ const response = await this.fetchPreviewQuote(coupon);
672
+ if (currentId !== this.previewRequestId)
673
+ return;
674
+ this.appliedCouponCode = coupon;
675
+ this.previewQuote = response.quote;
676
+ this.trackCouponApplied(coupon, response.quote);
677
+ }
678
+ catch (_err) {
679
+ if (currentId === this.previewRequestId) {
680
+ this.setCouponError();
681
+ this.trackCouponDenied(coupon);
276
682
  }
277
683
  }
684
+ finally {
685
+ if (currentId === this.previewRequestId) {
686
+ this.previewLoading = false;
687
+ }
688
+ }
689
+ }
690
+ async removeCartCoupon() {
691
+ this.requestId++;
692
+ const currentId = this.requestId;
693
+ const coupon = this.appliedCouponCode;
694
+ try {
695
+ this.quoteLoading = true;
696
+ const response = await this.fetchCartQuote("");
697
+ if (currentId !== this.requestId)
698
+ return;
699
+ this.couponInput = "";
700
+ this.appliedCouponCode = "";
701
+ this.previewQuote = null;
702
+ this.applyQuote(response.quote, currentId);
703
+ this.trackCouponRemoved(coupon, response.quote);
704
+ }
705
+ catch (_err) {
706
+ this.handleCouponError(currentId, "Failed to remove coupon. Please try again.");
707
+ }
278
708
  finally {
279
709
  if (currentId === this.requestId) {
280
710
  this.quoteLoading = false;
281
711
  }
282
712
  }
283
713
  }
714
+ handleCouponError(requestId, message) {
715
+ if (requestId !== this.requestId)
716
+ return;
717
+ this.setCouponError(message);
718
+ }
719
+ setCouponError(message = "Failed to apply coupon. Please check the code.") {
720
+ this.couponError = message;
721
+ this.errorOccurred.emit({ error: this.couponError });
722
+ }
723
+ trackCouponEntered(coupon) {
724
+ if (!coupon)
725
+ return;
726
+ this.trackEvent("Coupon Entered", { coupon });
727
+ }
728
+ trackCouponApplied(coupon, quote) {
729
+ if (!coupon)
730
+ return;
731
+ this.trackEvent("Coupon Applied", {
732
+ coupon,
733
+ products: this.getProductsForQuote(quote),
734
+ total: quote ? quoteTotal(quote) / 100 : 0,
735
+ value: quote ? quoteTotal(quote) / 100 : 0,
736
+ });
737
+ }
738
+ trackCouponDenied(coupon) {
739
+ if (!coupon)
740
+ return;
741
+ this.trackEvent("Coupon Denied", {
742
+ coupon,
743
+ reason: this.couponError,
744
+ });
745
+ }
746
+ trackCouponRemoved(coupon, quote) {
747
+ if (!coupon)
748
+ return;
749
+ this.trackEvent("Coupon Removed", {
750
+ coupon,
751
+ products: this.getProductsForQuote(quote),
752
+ total: quote ? quoteTotal(quote) / 100 : 0,
753
+ value: quote ? quoteTotal(quote) / 100 : 0,
754
+ });
755
+ }
756
+ handlePasscodeInput = (e) => {
757
+ if (!(e.target instanceof HTMLInputElement))
758
+ return;
759
+ this.passcodeInput = e.target.value;
760
+ };
761
+ async submitPasscode(e) {
762
+ e.preventDefault();
763
+ this.activePasscode = this.passcodeInput.trim();
764
+ await this.loadEventData();
765
+ }
284
766
  async initiateCheckout() {
767
+ if (this.isCheckoutDisabled())
768
+ return;
285
769
  if (!this.quote)
286
770
  return;
287
771
  try {
288
772
  this.checkoutLoading = true;
289
773
  this.quoteError = null;
290
- this.trackEvent("Checkout Started", {
291
- value: (this.quote.price?.total || 0) / 100,
292
- revenue: (this.quote.price?.total || 0) / 100,
293
- currency: this.event?.currency,
294
- products: this.quote.items.map((item) => {
295
- const admission = this.event?.admissions.find((a) => a.id === item.admissionId);
296
- const section = item.sectionId
297
- ? this.event?.sections.find((s) => s.id === item.sectionId)
298
- : null;
299
- return {
300
- product_id: item.admissionId,
301
- sku: item.admissionId,
302
- name: admission?.name,
303
- category: this.event?.name,
304
- variant: section?.name,
305
- price: ((item.price?.base || 0) + (item.price?.booking || 0)) / 100,
306
- quantity: item.quantity,
307
- admission_id: item.admissionId,
308
- section_id: item.sectionId,
309
- section_name: section?.name,
310
- price_base: item.price?.base,
311
- price_booking_fee: item.price?.booking,
312
- };
313
- }),
314
- });
774
+ this.trackCheckoutStarted(this.quote);
315
775
  const result = await this.client.order.create({
316
776
  eventId: this.eventId,
317
- items: this.quote.items.map((item) => ({
318
- quantity: item.quantity,
319
- admissionId: item.admissionId,
320
- sectionId: item.sectionId,
321
- })),
777
+ items: this.getCheckoutItems(this.quote),
778
+ couponCode: this.appliedCouponCode || undefined,
322
779
  returnUrl: this.returnUrl,
323
780
  });
324
- this.checkoutInit.emit({
325
- orderId: result.order.id,
326
- checkoutUrl: result.order.checkoutUrl,
327
- });
328
- if (result.order.checkoutUrl) {
329
- window.location.href = result.order.checkoutUrl;
330
- return;
331
- }
332
- this.checkoutLoading = false;
781
+ this.handleCheckoutOrder(result.order);
333
782
  }
334
783
  catch (_err) {
335
- this.quoteError = "Failed to initiate checkout. Please try again.";
336
- this.errorOccurred.emit({ error: this.quoteError });
337
- this.checkoutLoading = false;
784
+ this.handleCheckoutError();
338
785
  }
339
786
  }
340
- formatDateTimeRange(startDate, endDate) {
341
- const formatOptions = {
342
- weekday: "short",
343
- day: "numeric",
344
- month: "short",
345
- hour: "2-digit",
346
- minute: "2-digit",
787
+ trackCheckoutStarted(quote) {
788
+ const total = quoteTotal(quote) / 100;
789
+ this.trackEvent("Checkout Started", {
790
+ value: total,
791
+ revenue: total,
792
+ currency: this.event?.currency,
793
+ products: this.getCheckoutProducts(),
794
+ });
795
+ }
796
+ getCheckoutItems(quote) {
797
+ return quote.items.map((item) => ({
798
+ quantity: item.quantity,
799
+ admissionId: item.admissionId,
800
+ sectionId: item.sectionId,
801
+ }));
802
+ }
803
+ handleCheckoutOrder(order) {
804
+ this.checkoutInit.emit({
805
+ orderId: order.id,
806
+ checkoutUrl: order.checkoutUrl,
807
+ });
808
+ if (order.checkoutUrl) {
809
+ window.location.href = order.checkoutUrl;
810
+ return;
811
+ }
812
+ this.checkoutLoading = false;
813
+ }
814
+ handleCheckoutError() {
815
+ this.quoteError = "Failed to initiate checkout. Please try again.";
816
+ this.errorOccurred.emit({ error: this.quoteError });
817
+ this.checkoutLoading = false;
818
+ }
819
+ handleStickyCheckoutClick(state) {
820
+ if (state.isDoorOnly || !hasQuoteItems(this.quote)) {
821
+ this.promptTicketSelection();
822
+ this.host.scrollIntoView({
823
+ behavior: "smooth",
824
+ block: "start",
825
+ });
826
+ return;
827
+ }
828
+ this.initiateCheckout().catch(() => undefined);
829
+ }
830
+ getCartProducts() {
831
+ if (!this.quote)
832
+ return [];
833
+ return this.getProductsForQuote(this.quote);
834
+ }
835
+ getCheckoutProducts() {
836
+ if (!this.quote)
837
+ return [];
838
+ return this.quote.items.map((item) => ({
839
+ ...this.getQuoteProduct(item),
840
+ section_name: this.getQuoteSection(item)?.name,
841
+ }));
842
+ }
843
+ getProductsForQuote(quote) {
844
+ if (!quote)
845
+ return [];
846
+ return quote.items.map((item) => this.getQuoteProduct(item));
847
+ }
848
+ getQuoteProduct(item) {
849
+ return {
850
+ ...quoteProductIdentity(item),
851
+ ...quoteProductContext(this.quoteProductContext(item)),
852
+ ...quoteProductPrice(item),
853
+ };
854
+ }
855
+ quoteProductContext(item) {
856
+ return {
857
+ event: this.event,
858
+ admission: this.getQuoteAdmission(item),
859
+ section: this.getQuoteSection(item),
347
860
  };
348
- const startFormatted = new Date(startDate).toLocaleString(this.locale, formatOptions);
349
- const endFormatted = new Date(endDate).toLocaleString(this.locale, formatOptions);
350
- return `${startFormatted} - ${endFormatted}`;
861
+ }
862
+ getQuoteAdmission(item) {
863
+ return this.event?.admissions.find((adm) => adm.id === item.admissionId);
864
+ }
865
+ getQuoteSection(item) {
866
+ if (!item.sectionId)
867
+ return null;
868
+ return this.event?.sections.find((section) => section.id === item.sectionId);
869
+ }
870
+ formatDateTimeRange(startDate, endDate) {
871
+ return `${formatNaiveDateTime(startDate, "EEE, MMM d · h:mm a")} - ${formatNaiveDateTime(endDate, "h:mm a")}`;
351
872
  }
352
873
  formatPriceValue(priceInCents) {
353
874
  const currency = this.event?.currency;
@@ -380,6 +901,11 @@ export class SessionsTicketing {
380
901
  handleQuantityIncrease = async (admissionId, sectionId) => {
381
902
  await this.updateQuantity(admissionId, sectionId, 1);
382
903
  };
904
+ handleQuantityInput = async (admissionId, sectionId, e) => {
905
+ if (!(e.target instanceof HTMLInputElement))
906
+ return;
907
+ await this.setQuantity(admissionId, sectionId, Number(e.target.value) || 0);
908
+ };
383
909
  openModal = (admissionId, e) => {
384
910
  e.stopPropagation();
385
911
  this.openModalAdmissionId = admissionId;
@@ -400,91 +926,219 @@ export class SessionsTicketing {
400
926
  this.closeModal();
401
927
  }
402
928
  };
403
- renderQuantityControls(admission, sectionId) {
404
- const key = `${admission.id}::${sectionId || "none"}`;
929
+ renderQuantityControls(admission, sectionId, disabled, maxQuantity) {
930
+ const key = selectionKey(admission.id, sectionId);
405
931
  const quantity = this.selectedItems.get(key) || 0;
406
- const isUnlimited = admission.available === -1 || admission.capacity === -1;
407
- return (h("div", { class: "quantity-controls" }, h("button", { type: "button", class: "quantity-btn decrease", onClick: () => this.handleQuantityDecrease(admission.id, sectionId), disabled: quantity === 0, "aria-label": "Decrease quantity" }, "\u2212"), h("span", { class: "quantity-value" }, quantity), h("button", { type: "button", class: "quantity-btn increase", onClick: () => this.handleQuantityIncrease(admission.id, sectionId), disabled: !isUnlimited && quantity >= admission.available, "aria-label": "Increase quantity" }, "+")));
932
+ return (h("div", { class: "quantity-controls" }, h("button", { type: "button", class: "quantity-btn decrease", onClick: () => this.handleQuantityDecrease(admission.id, sectionId), disabled: disabled || quantity === 0, "aria-label": "Decrease quantity" }, "\u2212"), h("input", { class: "quantity-value", type: "number", min: "0", max: maxQuantity, inputmode: "numeric", value: quantity, onInput: (e) => this.handleQuantityInput(admission.id, sectionId, e), disabled: disabled, "aria-label": `Quantity for ${admission.name}` }), h("button", { type: "button", class: "quantity-btn increase", onClick: () => this.handleQuantityIncrease(admission.id, sectionId), disabled: disabled || quantity >= maxQuantity, "aria-label": "Increase quantity" }, "+")));
933
+ }
934
+ getMaxQuantity(admission, sectionId) {
935
+ if (!this.event)
936
+ return 0;
937
+ return getAdmissionDisplayState({
938
+ admission,
939
+ event: this.event,
940
+ section: this.getSection(sectionId),
941
+ }).maxQuantity;
942
+ }
943
+ getSection(sectionId) {
944
+ if (!sectionId)
945
+ return undefined;
946
+ return this.event?.sections.find((item) => item.id === sectionId);
408
947
  }
409
- renderEventTimeAndLocation() {
410
- if (!this.event?.entryStartsAt || !this.event?.entryEndsAt)
948
+ renderEventTimeAndLocation(event) {
949
+ return (h(h.Fragment, null, this.renderEventTime(event, "admission-time"), this.renderEventLocation(event, "admission-venue")));
950
+ }
951
+ renderEventTime(event, className) {
952
+ if (!event.entryStartsAt || !event.entryEndsAt)
953
+ return null;
954
+ return (h("div", { class: className }, CalendarIcon(), this.formatDateTimeRange(event.entryStartsAt, event.entryEndsAt)));
955
+ }
956
+ renderAdmissionTime(admission, className) {
957
+ if (!admission?.entryStartsAt || !admission.entryEndsAt)
958
+ return null;
959
+ return (h("div", { class: className }, CalendarIcon(), this.formatDateTimeRange(admission.entryStartsAt, admission.entryEndsAt)));
960
+ }
961
+ renderSectionLocation(section) {
962
+ const value = [section.location?.name, section.location?.city]
963
+ .filter(Boolean)
964
+ .join(", ");
965
+ if (!value)
966
+ return null;
967
+ return (h("div", { class: "section-venue" }, LocationIcon(), value));
968
+ }
969
+ renderEventLocation(event, className) {
970
+ if (!event.location)
411
971
  return null;
412
- return (h(h.Fragment, null, h("div", { class: "admission-time" }, CalendarIcon(), this.formatDateTimeRange(this.event.entryStartsAt, this.event.entryEndsAt)), this.event.location && (h("div", { class: "admission-venue" }, LocationIcon(), this.event.location.name))));
972
+ return (h("div", { class: className }, LocationIcon(), event.location.name));
973
+ }
974
+ renderPriceAndControls(admission, sectionId, state, mode) {
975
+ const key = selectionKey(admission.id, sectionId);
976
+ const price = getAdmissionPriceDisplay({
977
+ admission,
978
+ initialPrice: this.activeInitialAdmissionPrices()?.[key],
979
+ mode,
980
+ quoteItem: quoteItemsBySelection(this.quote).get(key),
981
+ previewItem: quoteItemsBySelection(this.previewQuote).get(key),
982
+ });
983
+ const savings = admissionPriceSavings(price);
984
+ return (h("div", { class: "admission-price-info" }, h("div", { class: "admission-price-wrapper" }, savings > 0 && (h("span", { class: "admission-savings" }, "Save ", this.formatPriceValue(savings))), this.renderAdmissionPrice(price), price.fees > 0 && (h("span", { class: "admission-booking-fee" }, this.formatPriceValue(price.fees), " Booking Fee")), state.showScarcity && (h("span", { class: "admission-availability" }, admission.available, " tickets remaining")), state.label && (h("span", { class: `admission-sale-label ${state.label.tone}` }, state.label.text))), this.renderAdmissionControls(admission, sectionId, state, mode)));
413
985
  }
414
- renderPriceAndControls(admission, sectionId, isSoldOut) {
415
- const basePrice = admission.price?.base || 0;
416
- const bookingFee = admission.price?.booking || 0;
417
- return (h("div", { class: "admission-price-info" }, h("div", { class: "admission-price-wrapper" }, h("span", { class: "admission-price" }, this.formatPriceValue(basePrice)), h("span", { class: "admission-fee" }, bookingFee > 0 ? "+ Booking fee" : "No booking fee")), !isSoldOut ? (this.renderQuantityControls(admission, sectionId)) : (h("div", { class: "sold-out-controls" }, h("span", { class: "sold-out-text" }, "Sold Out")))));
986
+ renderAdmissionPrice(price) {
987
+ if (price.discounted) {
988
+ return (h("span", { class: "admission-price-row" }, h("span", { class: "admission-price discounted" }, this.formatPriceValue(price.base)), price.gross !== undefined && (h("span", { class: "admission-price-original" }, this.formatPriceValue(price.gross)))));
989
+ }
990
+ return (h("span", { class: "admission-price" }, this.formatPriceValue(price.base)));
418
991
  }
419
- renderAdmission(admission, section, isSectionSoldOut = false) {
420
- const key = `${admission.id}::${section?.id || "none"}`;
421
- const basePrice = admission.price?.base || 0;
422
- const isSoldOut = admission.available === 0 || isSectionSoldOut;
992
+ renderAdmissionControls(admission, sectionId, state, mode) {
993
+ if (state.statusLabel) {
994
+ return (h("div", { class: `sold-out-controls ${state.statusTone ?? ""}` }, h("span", { class: "sold-out-text" }, state.statusLabel)));
995
+ }
996
+ if (mode === "door")
997
+ return null;
998
+ return this.renderQuantityControls(admission, sectionId, state.disabled, state.maxQuantity);
999
+ }
1000
+ renderAdmission(admission, event, section, isSectionSoldOut = false, mode = "default") {
1001
+ const state = getAdmissionDisplayState({
1002
+ admission,
1003
+ event,
1004
+ section,
1005
+ sectionSoldOut: isSectionSoldOut,
1006
+ });
423
1007
  const sectionId = section?.id || null;
424
- const isStandaloneAdmission = !section;
425
- const ariaLabel = section
426
- ? `${admission.name} in ${section.name} - ${isSoldOut ? "Sold Out" : this.formatPriceValue(basePrice)}`
427
- : `${admission.name} - ${isSoldOut ? "Sold Out" : this.formatPriceValue(basePrice)}`;
428
- return (h("section", { class: `admission-card ${isSoldOut ? "sold-out" : ""} ${isStandaloneAdmission ? "solo-admission" : ""}`, key: key, tabindex: isSoldOut ? -1 : 0, "aria-label": ariaLabel }, h("div", { class: "admission-content" }, h("div", { class: "admission-details" }, h("div", { class: "admission-header" }, h("div", { class: "admission-header-text" }, h("h3", { class: "admission-title" }, admission.name), admission.subtitle && (h("p", { class: "admission-subtitle" }, admission.subtitle))), admission.description && (h("button", { type: "button", class: "more-info-badge", onClick: (e) => this.openModal(admission.id, e), "aria-label": `More info about ${admission.name}` }, InfoIcon(), h("span", null, "More info")))), isStandaloneAdmission && (h(h.Fragment, null, this.renderEventTimeAndLocation(), this.renderPriceAndControls(admission, sectionId, isSoldOut))), section &&
429
- this.renderPriceAndControls(admission, sectionId, isSoldOut)))));
430
- }
431
- renderSectionHeader(section, isSectionSoldOut) {
432
- return (h("div", { class: "section-header" }, h("h2", { class: "section-name" }, section.name, isSectionSoldOut && h("span", { class: "sold-out-badge" }, "Sold Out")), this.event?.entryStartsAt && this.event?.entryEndsAt && (h("div", { class: "section-time" }, CalendarIcon(), this.formatDateTimeRange(this.event.entryStartsAt, this.event.entryEndsAt))), this.event?.location && (h("div", { class: "section-venue" }, LocationIcon(), this.event.location.name))));
433
- }
434
- renderSectionWithAdmissions(section) {
435
- const sectionAdmissions = this.event?.admissions.filter((adm) => adm.sectionId === section.id) ||
436
- [];
437
- if (sectionAdmissions.length === 0)
1008
+ return (h("section", { class: this.getAdmissionClassName(state, section), key: selectionKey(admission.id, sectionId), tabindex: state.disabled ? -1 : 0, "aria-label": this.getAdmissionAriaLabel(admission, section, state) }, h("div", { class: "admission-content" }, h("div", { class: "admission-details" }, this.renderAdmissionHeader(admission), this.renderAdmissionBody(admission, event, sectionId, state, mode)))));
1009
+ }
1010
+ getAdmissionClassName(state, section) {
1011
+ return `admission-card ${state.soldOut ? "sold-out" : ""} ${state.disabled && !state.soldOut ? "unavailable" : ""} ${section ? "" : "solo-admission"}`;
1012
+ }
1013
+ getAdmissionAriaLabel(admission, section, state) {
1014
+ const status = state.statusLabel ?? this.admissionStatusText(admission, state.soldOut);
1015
+ return admissionAriaLabel(admission, section, status);
1016
+ }
1017
+ admissionStatusText(admission, soldOut) {
1018
+ if (soldOut)
1019
+ return "Sold Out";
1020
+ if ((admission.price.booking ?? 0) <= 0) {
1021
+ return this.formatPriceValue(admission.price.base);
1022
+ }
1023
+ return `${this.formatPriceValue(admission.price.base)} plus ${this.formatPriceValue(admission.price.booking)} booking fee`;
1024
+ }
1025
+ renderAdmissionHeader(admission) {
1026
+ return (h("div", { class: "admission-header" }, h("div", { class: "admission-header-text" }, h("h3", { class: "admission-title" }, admission.name), admission.subtitle && (h("p", { class: "admission-subtitle" }, admission.subtitle))), this.renderMoreInfoButton(admission)));
1027
+ }
1028
+ renderMoreInfoButton(admission) {
1029
+ if (!admission.description)
438
1030
  return null;
439
- const isSectionSoldOut = section.available === 0;
440
- return (h("div", { class: `ticket-section ${isSectionSoldOut ? "section-sold-out" : ""}`, key: section.id }, this.renderSectionHeader(section, isSectionSoldOut), h("div", { class: "admissions-list" }, sectionAdmissions
441
- .sort((a, b) => a.order - b.order)
442
- .map((admission) => this.renderAdmission(admission, section, isSectionSoldOut)))));
1031
+ return (h("button", { type: "button", class: "more-info-badge", onClick: (e) => this.openModal(admission.id, e), "aria-label": `More info about ${admission.name}` }, InfoIcon(), h("span", null, "More info")));
1032
+ }
1033
+ renderAdmissionBody(admission, event, sectionId, state, mode) {
1034
+ if (sectionId) {
1035
+ return this.renderPriceAndControls(admission, sectionId, state, mode);
1036
+ }
1037
+ return (h(h.Fragment, null, this.renderEventTimeAndLocation(event), this.renderPriceAndControls(admission, sectionId, state, mode)));
1038
+ }
1039
+ renderSectionHeader(section, admissions, isSectionSoldOut) {
1040
+ return (h("div", { class: "section-header" }, h("h2", { class: "section-name" }, section.name, isSectionSoldOut && h("span", { class: "sold-out-badge" }, "Sold Out")), this.renderAdmissionTime(admissions[0], "section-time"), this.renderSectionLocation(section)));
1041
+ }
1042
+ renderSectionWithAdmissions(section, admissions, event, mode) {
1043
+ const soldOut = isSectionSoldOut({
1044
+ admissions,
1045
+ section,
1046
+ });
1047
+ return (h("div", { class: `ticket-section ${soldOut ? "section-sold-out" : ""}`, key: section.id }, this.renderSectionHeader(section, admissions, soldOut), h("div", { class: "admissions-list" }, admissions.map((admission) => this.renderAdmission(admission, event, section, soldOut, mode)))));
443
1048
  }
444
1049
  renderLoadingState() {
445
1050
  return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "loading-state" }, "Loading event details...")));
446
1051
  }
447
1052
  renderErrorState() {
1053
+ if (this.passcodeDenied)
1054
+ return this.renderPasscodeState();
448
1055
  return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "error-banner" }, h("strong", null, "Unable to load event"), h("p", null, this.error), h("button", { type: "button", class: "retry-button", onClick: () => this.loadEventData() }, "Try Again")))));
449
1056
  }
1057
+ renderPasscodeState() {
1058
+ return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("form", { class: "passcode-form", onSubmit: (e) => this.submitPasscode(e) }, h("h2", null, "Enter passcode"), h("p", null, "Enter the event passcode to view tickets and event details."), h("input", { type: "text", name: "passcode", value: this.passcodeInput, onInput: this.handlePasscodeInput, autocomplete: "one-time-code", placeholder: "Passcode", "aria-label": "Passcode" }), this.passcodeDenied && this.activePasscode && (h("span", { class: "error-message" }, "Incorrect passcode")), h("button", { type: "submit", class: "checkout-btn", disabled: !this.passcodeInput.trim() }, "Unlock event")))));
1059
+ }
450
1060
  renderNotFoundState() {
451
1061
  return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("div", { class: "error-banner" }, h("strong", null, "Event not found"), h("p", null, "The requested event could not be found.")))));
452
1062
  }
453
- renderOrderSummaryItems() {
454
- if (!this.quote?.items?.length) {
455
- return (h("div", { class: "empty-cart-message" }, h("span", { class: "empty-cart-text" }, "No tickets selected"), h("span", { class: "empty-cart-price" }, this.formatPriceValue(0))));
456
- }
457
- return (h(h.Fragment, null, this.quote.items.map((item) => {
458
- const admission = this.event?.admissions.find((a) => a.id === item.admissionId);
459
- const section = item.sectionId
460
- ? this.event?.sections.find((s) => s.id === item.sectionId)
461
- : null;
462
- const itemTotal = item.price?.base || 0;
463
- if (section) {
464
- return (h("div", { class: "order-item-with-section", key: `${item.admissionId}::${item.sectionId || "none"}` }, h("div", { class: "item-section-line" }, h("div", { class: "item-section-name" }, item.quantity, " x ", section.name), h("div", { class: "item-price" }, this.formatPriceValue(itemTotal))), h("div", { class: "item-admission-name" }, admission?.name)));
465
- }
466
- return (h("div", { class: "order-item", key: `${item.admissionId}::${item.sectionId || "none"}` }, h("div", { class: "item-description" }, item.quantity, " x ", admission?.name), h("div", { class: "item-price" }, this.formatPriceValue(itemTotal))));
467
- })));
1063
+ renderCouponPanel() {
1064
+ const hasInput = this.couponInput.trim().length > 0;
1065
+ const busy = this.quoteLoading || this.previewLoading || this.checkoutLoading;
1066
+ const canApply = hasInput && !busy;
1067
+ return (h("div", { class: "coupon-panel" }, h("div", { class: "coupon-row" }, h("input", { id: "coupon-code", name: "coupon-code", placeholder: "Coupon Code", value: this.couponInput, onInput: this.handleCouponInput, onKeyDown: this.handleCouponKeyDown, autocomplete: "off", disabled: busy, "aria-label": "Coupon code", "aria-invalid": Boolean(this.couponError) }), this.appliedCouponCode ? (h("button", { type: "button", class: "coupon-remove", onClick: () => this.removeCoupon(), disabled: busy, "aria-label": "Remove coupon" }, "Remove")) : (h("button", { type: "button", class: "coupon-apply", onClick: () => this.applyCoupon(), disabled: !canApply }, "Apply"))), this.couponError && (h("span", { class: "error-message" }, this.couponError))));
468
1068
  }
469
1069
  isCheckoutDisabled() {
470
1070
  return (this.quoteLoading ||
471
1071
  this.checkoutLoading ||
472
- !this.quote ||
473
- !this.quote.items?.length);
1072
+ Boolean(this.quoteError) ||
1073
+ !hasQuoteItems(this.quote));
474
1074
  }
475
1075
  getCheckoutButtonText() {
476
1076
  if (this.checkoutLoading)
477
1077
  return "Processing...";
1078
+ if (this.quote)
1079
+ return this.getQuoteCheckoutText();
1080
+ return "Pay";
1081
+ }
1082
+ renderQuoteLoadingContent() {
1083
+ return (h("span", { class: "checkout-spinner-wrapper", "aria-hidden": "true" }, LoaderCircleIcon()));
1084
+ }
1085
+ renderCheckoutButtonContent() {
478
1086
  if (this.quoteLoading)
479
- return "Updating...";
480
- return "Checkout";
1087
+ return this.renderQuoteLoadingContent();
1088
+ return this.getCheckoutButtonText();
1089
+ }
1090
+ isStickyCheckoutDisabled(state) {
1091
+ if (state.isDoorOnly)
1092
+ return false;
1093
+ return (state.isEnded ||
1094
+ state.isUnavailableForSale ||
1095
+ this.checkoutLoading ||
1096
+ this.quoteLoading);
1097
+ }
1098
+ getStickyCheckoutText(state) {
1099
+ if (this.checkoutLoading)
1100
+ return "Processing...";
1101
+ if (this.quote)
1102
+ return this.getQuoteCheckoutText();
1103
+ if (this.selectionPrompt)
1104
+ return "Select a ticket";
1105
+ if (state.isDoorOnly)
1106
+ return "Get Tickets at the Door";
1107
+ if (state.isEnded)
1108
+ return "Event Ended";
1109
+ if (state.isUnavailableForSale)
1110
+ return "Tickets Unavailable";
1111
+ return "Get Tickets";
481
1112
  }
482
- renderOrderSummary() {
483
- return (h("div", { class: "order-summary" }, h("h2", { class: "summary-title" }, "Order summary"), this.quoteError && h("div", { class: "error-message" }, this.quoteError), h("div", { class: "order-items" }, this.renderOrderSummaryItems()), h("div", { class: "order-fees" }, h("div", { class: "fee-row" }, h("span", null, "Booking fee"), h("span", null, this.formatPriceValue(this.quote?.price?.booking || 0)))), h("div", { class: "order-total" }, h("span", null, "Total price"), h("span", null, !this.quote
484
- ? "N/A"
485
- : (this.quote.price?.total || 0) === 0
486
- ? "Free"
487
- : this.formatPriceValue(this.quote.price?.total || 0))), h("button", { type: "button", class: "checkout-btn", onClick: () => this.initiateCheckout(), disabled: this.isCheckoutDisabled() }, this.getCheckoutButtonText())));
1113
+ renderStickyCheckoutButtonContent(state) {
1114
+ if (this.quoteLoading)
1115
+ return this.renderQuoteLoadingContent();
1116
+ return this.getStickyCheckoutText(state);
1117
+ }
1118
+ getQuoteCheckoutText() {
1119
+ if (!this.quote)
1120
+ return "Pay";
1121
+ if (quoteTotal(this.quote) === 0)
1122
+ return "Continue";
1123
+ return `Pay ${this.getOrderTotalText()}`;
1124
+ }
1125
+ getOrderTotalText() {
1126
+ if (!this.quote)
1127
+ return "N/A";
1128
+ const total = quoteTotal(this.quote);
1129
+ if (total === 0)
1130
+ return "Free";
1131
+ return this.formatPriceValue(total);
1132
+ }
1133
+ renderCheckoutActions() {
1134
+ return (h("div", { class: "checkout-actions" }, this.renderCouponPanel(), this.quoteError && h("div", { class: "error-message" }, this.quoteError), h("button", { type: "button", class: "checkout-btn", onClick: () => this.initiateCheckout(), disabled: this.isCheckoutDisabled(), "aria-label": this.quoteLoading ? "Updating" : undefined }, this.renderCheckoutButtonContent())));
1135
+ }
1136
+ renderStickyCheckout(state) {
1137
+ if (!this.stickyCheckout)
1138
+ return null;
1139
+ if (!state.showTicketing)
1140
+ return null;
1141
+ return (h("div", { class: "sticky-checkout-bar" }, h("button", { type: "button", class: `checkout-btn sticky-checkout-btn ${this.selectionPrompt ? "selection-prompt" : ""}`, onClick: () => this.handleStickyCheckoutClick(state), disabled: this.isStickyCheckoutDisabled(state), "aria-label": this.quoteLoading ? "Updating" : undefined }, this.renderStickyCheckoutButtonContent(state))));
488
1142
  }
489
1143
  renderModal() {
490
1144
  if (!this.openModalAdmissionId)
@@ -496,22 +1150,60 @@ export class SessionsTicketing {
496
1150
  // biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: backdrop click-to-close is a common modal pattern, keyboard handled via Escape key
497
1151
  h("div", { class: "modal-backdrop", onClick: this.handleBackdropClick }, h("div", { class: "modal-content", role: "dialog", "aria-modal": "true", "aria-labelledby": "modal-title" }, h("button", { type: "button", class: "modal-close-btn", onClick: () => this.closeModal(), "aria-label": "Close" }, CloseIcon()), h("div", { class: "modal-header" }, h("h2", { id: "modal-title", class: "modal-title" }, admission.name), admission.subtitle && (h("p", { class: "modal-subtitle" }, admission.subtitle))), h("p", { class: "modal-description" }, admission.description))));
498
1152
  }
499
- renderTicketSections() {
500
- const generalAdmissions = this.event?.admissions.filter((adm) => !adm.sectionId) || [];
501
- return (h("div", { class: "tickets-wrapper" }, generalAdmissions.length > 0 && (h("div", { class: "unsectioned-admissions" }, generalAdmissions
502
- .sort((a, b) => a.order - b.order)
503
- .map((admission) => (h("div", { class: "ticket-section" }, h("div", { class: "admissions-list" }, this.renderAdmission(admission))))))), this.event?.sections
504
- .sort((a, b) => a.order - b.order)
505
- .map((section) => this.renderSectionWithAdmissions(section))));
1153
+ renderTicketSections(event) {
1154
+ const state = getTicketingState(event);
1155
+ const mode = state.isDoorOnly ? "door" : "default";
1156
+ const items = topLevelTicketItems(event.sections, visibleAdmissions(event, mode));
1157
+ return (h("div", { class: "tickets-wrapper" }, this.renderDoorAlert(event, mode), h("h2", { class: "tickets-heading" }, state.ticketHeading), items.map((item) => this.renderTopLevelItem(item, event, mode))));
506
1158
  }
507
- render() {
508
- if (this.loading)
509
- return this.renderLoadingState();
510
- if (this.error && !this.event)
511
- return this.renderErrorState();
512
- if (!this.event)
1159
+ renderTopLevelItem(item, event, mode) {
1160
+ if (item.type === "section") {
1161
+ return this.renderSectionWithAdmissions(item.section, item.admissions, event, mode);
1162
+ }
1163
+ return (h("div", { class: "ticket-section", key: item.admission.id }, h("div", { class: "admissions-list" }, this.renderAdmission(item.admission, event, undefined, false, mode))));
1164
+ }
1165
+ renderDoorAlert(event, mode) {
1166
+ if (mode !== "door")
1167
+ return null;
1168
+ const location = event.location?.name?.trim() || "the venue";
1169
+ return (h("div", { class: "door-alert" }, h("strong", null, "Tickets are available at ", location), h("span", null, "Online sales are closed. Buy these tickets at the door when you arrive.")));
1170
+ }
1171
+ loadingView() {
1172
+ return this.loading ? this.renderLoadingState() : undefined;
1173
+ }
1174
+ errorView() {
1175
+ return this.error && !this.event ? this.renderErrorState() : undefined;
1176
+ }
1177
+ missingEventView() {
1178
+ return this.event ? undefined : this.renderNotFoundState();
1179
+ }
1180
+ pendingView() {
1181
+ return this.loadingView() ?? this.errorView() ?? this.missingEventView();
1182
+ }
1183
+ renderReadyState() {
1184
+ const event = this.event;
1185
+ if (!event)
513
1186
  return this.renderNotFoundState();
514
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, this.renderTicketSections(), this.renderOrderSummary()), this.renderModal()));
1187
+ const state = getTicketingState(event);
1188
+ if (state.showExternalTicketing) {
1189
+ return this.renderExternalTicketing(state.externalTicketUrl);
1190
+ }
1191
+ return (h(Host, { "data-theme": this.currentTheme, "data-sticky-checkout": this.stickyCheckout ? "true" : undefined }, h("div", { class: "session-services-ticketing-container" }, state.showTicketing
1192
+ ? this.renderTicketSections(event)
1193
+ : this.renderUnavailableState(state), !state.isDoorOnly &&
1194
+ state.showTicketing &&
1195
+ this.renderCheckoutActions()), this.renderStickyCheckout(state), this.renderModal()));
1196
+ }
1197
+ renderExternalTicketing(url) {
1198
+ return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, h("a", { class: "external-ticket-button", href: url, target: "_blank", rel: "noreferrer" }, "Get tickets"))));
1199
+ }
1200
+ renderUnavailableState(state) {
1201
+ return (h("div", { class: "empty-ticketing-state" }, h("strong", null, state.isEnded ? "Event ended" : "Tickets unavailable"), h("span", null, state.isEnded
1202
+ ? "Ticket sales are closed for this event."
1203
+ : "Tickets are not currently available for this event.")));
1204
+ }
1205
+ render() {
1206
+ return this.pendingView() ?? this.renderReadyState();
515
1207
  }
516
1208
  static get is() { return "session-services-ticketing"; }
517
1209
  static get encapsulation() { return "shadow"; }
@@ -624,6 +1316,113 @@ export class SessionsTicketing {
624
1316
  "reflect": false,
625
1317
  "attribute": "return-url"
626
1318
  },
1319
+ "couponCode": {
1320
+ "type": "string",
1321
+ "mutable": false,
1322
+ "complexType": {
1323
+ "original": "string",
1324
+ "resolved": "string",
1325
+ "references": {}
1326
+ },
1327
+ "required": false,
1328
+ "optional": true,
1329
+ "docs": {
1330
+ "tags": [],
1331
+ "text": ""
1332
+ },
1333
+ "getter": false,
1334
+ "setter": false,
1335
+ "reflect": false,
1336
+ "attribute": "coupon-code"
1337
+ },
1338
+ "initialAdmissionPrices": {
1339
+ "type": "string",
1340
+ "mutable": false,
1341
+ "complexType": {
1342
+ "original": "| Record<string, { discount: number; gross: number; total: number }>\n | string",
1343
+ "resolved": "string | { [x: string]: { discount: number; gross: number; total: number; }; }",
1344
+ "references": {
1345
+ "Record": {
1346
+ "location": "global",
1347
+ "id": "global::Record"
1348
+ }
1349
+ }
1350
+ },
1351
+ "required": false,
1352
+ "optional": true,
1353
+ "docs": {
1354
+ "tags": [],
1355
+ "text": ""
1356
+ },
1357
+ "getter": false,
1358
+ "setter": false,
1359
+ "reflect": false,
1360
+ "attribute": "initial-admission-prices"
1361
+ },
1362
+ "initialPreviewQuote": {
1363
+ "type": "string",
1364
+ "mutable": false,
1365
+ "complexType": {
1366
+ "original": "OrderQuote | string",
1367
+ "resolved": "string | { items: { id: string; kind: \"ADMISSION\" | \"ADDON\"; quantity: number; price: { net: number; total: number; gross: number; base: number; booking: number; refunded: number; discount: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; admissionId?: string; sectionId?: string; admission?: { name: string; capacity: number; limit: number; order: number; entryStartsAt: string; entryEndsAt: string; saleStartsAt: string; saleEndsAt: string; saleEndsBehavior: \"SALE_ENDED\" | \"SOLD_OUT\" | \"DOOR\"; price: { base: number; booking: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; visibility: \"ALWAYS\" | \"STACKED\" | \"HIDDEN\"; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; subtitle?: string; description?: string; available?: number; sold?: number; reserved?: number; createdAt?: string; updatedAt?: string; passcode?: string; sectionId?: string; }; section?: { name: string; capacity: number; limit: number; order: number; location: { createdAt: string; updatedAt: string; name: string; address: string; country: string; latitude: number; longitude: number; timeZone: string; id?: string; googlePlaceId?: string; city?: string; state?: string; postcode?: string; types?: string[]; description?: string; image?: { src: string; alt: string; width: number; height: number; }; venueId?: string; }; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; createdAt?: string; updatedAt?: string; available?: number; sold?: number; reserved?: number; passcode?: string; }; }[]; price: { net: number; total: number; gross: number; base: number; booking: number; shipping: number; sms: number; refunded: number; discount: number; gateway: number; dispute: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; couponCode?: { name: string; active: boolean; couponId: string; teamId: string; id?: string; createdAt?: string; updatedAt?: string; uses?: number; }; }",
1368
+ "references": {
1369
+ "OrderQuote": {
1370
+ "location": "import",
1371
+ "path": "@session-services/sdk/schemas",
1372
+ "id": "../../sdk/src/schemas.ts::OrderQuote"
1373
+ }
1374
+ }
1375
+ },
1376
+ "required": false,
1377
+ "optional": true,
1378
+ "docs": {
1379
+ "tags": [],
1380
+ "text": ""
1381
+ },
1382
+ "getter": false,
1383
+ "setter": false,
1384
+ "reflect": false,
1385
+ "attribute": "initial-preview-quote"
1386
+ },
1387
+ "passcode": {
1388
+ "type": "string",
1389
+ "mutable": false,
1390
+ "complexType": {
1391
+ "original": "string",
1392
+ "resolved": "string",
1393
+ "references": {}
1394
+ },
1395
+ "required": false,
1396
+ "optional": true,
1397
+ "docs": {
1398
+ "tags": [],
1399
+ "text": ""
1400
+ },
1401
+ "getter": false,
1402
+ "setter": false,
1403
+ "reflect": false,
1404
+ "attribute": "passcode"
1405
+ },
1406
+ "stickyCheckout": {
1407
+ "type": "boolean",
1408
+ "mutable": false,
1409
+ "complexType": {
1410
+ "original": "boolean",
1411
+ "resolved": "boolean",
1412
+ "references": {}
1413
+ },
1414
+ "required": false,
1415
+ "optional": false,
1416
+ "docs": {
1417
+ "tags": [],
1418
+ "text": ""
1419
+ },
1420
+ "getter": false,
1421
+ "setter": false,
1422
+ "reflect": false,
1423
+ "attribute": "sticky-checkout",
1424
+ "defaultValue": "false"
1425
+ },
627
1426
  "theme": {
628
1427
  "type": "string",
629
1428
  "mutable": false,
@@ -691,10 +1490,19 @@ export class SessionsTicketing {
691
1490
  "event": {},
692
1491
  "selectedItems": {},
693
1492
  "quote": {},
1493
+ "previewQuote": {},
694
1494
  "loading": {},
695
1495
  "error": {},
696
1496
  "quoteLoading": {},
1497
+ "previewLoading": {},
697
1498
  "quoteError": {},
1499
+ "couponError": {},
1500
+ "couponInput": {},
1501
+ "appliedCouponCode": {},
1502
+ "passcodeInput": {},
1503
+ "activePasscode": {},
1504
+ "passcodeDenied": {},
1505
+ "selectionPrompt": {},
698
1506
  "checkoutLoading": {},
699
1507
  "currentTheme": {},
700
1508
  "openModalAdmissionId": {}
@@ -728,7 +1536,7 @@ export class SessionsTicketing {
728
1536
  },
729
1537
  "complexType": {
730
1538
  "original": "OrderQuote",
731
- "resolved": "{ items: { id: string; kind: \"ADMISSION\" | \"ADDON\"; quantity: number; price: { net: number; total: number; gross: number; base: number; booking: number; refunded: number; discount: number; currency: string; }; admissionId?: string; sectionId?: string; admission?: { name: string; capacity: number; limit: number; order: number; entryStartsAt: string; entryEndsAt: string; saleStartsAt: string; saleEndsAt: string; price: { base: number; booking: number; currency: string; }; visibility: \"ALWAYS\" | \"STACKED\" | \"HIDDEN\"; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; subtitle?: string; description?: string; available?: number; sold?: number; reserved?: number; createdAt?: string; updatedAt?: string; passcode?: string; sectionId?: string; }; section?: { name: string; capacity: number; limit: number; order: number; location: { createdAt: string; updatedAt: string; name: string; address: string; country: string; latitude: number; longitude: number; timeZone: string; id?: string; googlePlaceId?: string; city?: string; state?: string; postcode?: string; types?: string[]; description?: string; image?: { src: string; alt: string; width: number; height: number; }; venueId?: string; }; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; createdAt?: string; updatedAt?: string; available?: number; sold?: number; reserved?: number; passcode?: string; }; }[]; price: { net: number; total: number; gross: number; base: number; booking: number; shipping: number; refunded: number; discount: number; gateway: number; dispute: number; currency: string; }; couponCode?: { name: string; active: boolean; couponId: string; teamId: string; id?: string; createdAt?: string; updatedAt?: string; uses?: number; }; }",
1539
+ "resolved": "{ items: { id: string; kind: \"ADMISSION\" | \"ADDON\"; quantity: number; price: { net: number; total: number; gross: number; base: number; booking: number; refunded: number; discount: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; admissionId?: string; sectionId?: string; admission?: { name: string; capacity: number; limit: number; order: number; entryStartsAt: string; entryEndsAt: string; saleStartsAt: string; saleEndsAt: string; saleEndsBehavior: \"SALE_ENDED\" | \"SOLD_OUT\" | \"DOOR\"; price: { base: number; booking: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; visibility: \"ALWAYS\" | \"STACKED\" | \"HIDDEN\"; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; subtitle?: string; description?: string; available?: number; sold?: number; reserved?: number; createdAt?: string; updatedAt?: string; passcode?: string; sectionId?: string; }; section?: { name: string; capacity: number; limit: number; order: number; location: { createdAt: string; updatedAt: string; name: string; address: string; country: string; latitude: number; longitude: number; timeZone: string; id?: string; googlePlaceId?: string; city?: string; state?: string; postcode?: string; types?: string[]; description?: string; image?: { src: string; alt: string; width: number; height: number; }; venueId?: string; }; teamId: string; promoterId: string; eventId: string; eventSlug: string; id?: string; createdAt?: string; updatedAt?: string; available?: number; sold?: number; reserved?: number; passcode?: string; }; }[]; price: { net: number; total: number; gross: number; base: number; booking: number; shipping: number; sms: number; refunded: number; discount: number; gateway: number; dispute: number; currency: \"AUD\" | \"AED\" | \"ALL\" | \"AMD\" | \"ANG\" | \"AOA\" | \"ARS\" | \"AWG\" | \"AZN\" | \"BAM\" | \"BBD\" | \"BDT\" | \"BGN\" | \"BHD\" | \"BIF\" | \"BMD\" | \"BND\" | \"BOB\" | \"BRL\" | \"BSD\" | \"BTN\" | \"BWP\" | \"BZD\" | \"CAD\" | \"CHF\" | \"CLP\" | \"CNY\" | \"COP\" | \"CRC\" | \"CVE\" | \"CZK\" | \"DJF\" | \"DKK\" | \"DOP\" | \"DZD\" | \"EGP\" | \"ERN\" | \"ETB\" | \"EUR\" | \"FJD\" | \"FKP\" | \"GBP\" | \"GEL\" | \"GHS\" | \"GIP\" | \"GMD\" | \"GNF\" | \"GTQ\" | \"GYD\" | \"HKD\" | \"HNL\" | \"HUF\" | \"IDR\" | \"ILS\" | \"INR\" | \"ISK\" | \"JMD\" | \"JOD\" | \"JPY\" | \"KES\" | \"KGS\" | \"KHR\" | \"KMF\" | \"KRW\" | \"KWD\" | \"KYD\" | \"KZT\" | \"LAK\" | \"LBP\" | \"LKR\" | \"LRD\" | \"LSL\" | \"LYD\" | \"MAD\" | \"MDL\" | \"MGA\" | \"MKD\" | \"MNT\" | \"MOP\" | \"MUR\" | \"MVR\" | \"MWK\" | \"MXN\" | \"MYR\" | \"NAD\" | \"NGN\" | \"NIO\" | \"NOK\" | \"NPR\" | \"NZD\" | \"OMR\" | \"PAB\" | \"PEN\" | \"PGK\" | \"PHP\" | \"PKR\" | \"PLN\" | \"PYG\" | \"QAR\" | \"RON\" | \"RSD\" | \"RWF\" | \"SAR\" | \"SBD\" | \"SCR\" | \"SEK\" | \"SGD\" | \"SHP\" | \"SLE\" | \"SRD\" | \"SVC\" | \"SZL\" | \"THB\" | \"TJS\" | \"TMT\" | \"TND\" | \"TOP\" | \"TRY\" | \"TTD\" | \"TWD\" | \"TZS\" | \"UAH\" | \"UGX\" | \"USD\" | \"UYU\" | \"UZS\" | \"VND\" | \"VUV\" | \"WST\" | \"XAF\" | \"XCD\" | \"XOF\" | \"XPF\" | \"ZAR\" | \"ZMW\" | \"ZWL\"; }; couponCode?: { name: string; active: boolean; couponId: string; teamId: string; id?: string; createdAt?: string; updatedAt?: string; uses?: number; }; }",
732
1540
  "references": {
733
1541
  "OrderQuote": {
734
1542
  "location": "import",
@@ -754,6 +1562,7 @@ export class SessionsTicketing {
754
1562
  }
755
1563
  }];
756
1564
  }
1565
+ static get elementRef() { return "host"; }
757
1566
  static get watchers() {
758
1567
  return [{
759
1568
  "propName": "tenantId",
@@ -764,6 +1573,18 @@ export class SessionsTicketing {
764
1573
  }, {
765
1574
  "propName": "apiUrl",
766
1575
  "methodName": "propsChanged"
1576
+ }, {
1577
+ "propName": "passcode",
1578
+ "methodName": "propsChanged"
1579
+ }, {
1580
+ "propName": "couponCode",
1581
+ "methodName": "couponCodeChanged"
1582
+ }, {
1583
+ "propName": "initialAdmissionPrices",
1584
+ "methodName": "couponCodeChanged"
1585
+ }, {
1586
+ "propName": "initialPreviewQuote",
1587
+ "methodName": "couponCodeChanged"
767
1588
  }, {
768
1589
  "propName": "theme",
769
1590
  "methodName": "themeChanged"