@session-services/web-elements 0.1.20 → 0.1.21

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 +24398 -19334
  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 +401 -339
  8. package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +1087 -269
  9. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +263 -0
  10. package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +276 -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 +24442 -19360
  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 +24406 -19342
  19. package/dist/session-services-elements/{p-a2404c46.entry.js → p-703444ed.entry.js} +1 -1
  20. package/dist/session-services-elements/p-CNOpF6tk.js +2 -0
  21. package/dist/session-services-elements/p-b8703bf3.entry.js +1 -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 +193 -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),
347
853
  };
348
- const startFormatted = new Date(startDate).toLocaleString(this.locale, formatOptions);
349
- const endFormatted = new Date(endDate).toLocaleString(this.locale, formatOptions);
350
- return `${startFormatted} - ${endFormatted}`;
854
+ }
855
+ quoteProductContext(item) {
856
+ return {
857
+ event: this.event,
858
+ admission: this.getQuoteAdmission(item),
859
+ section: this.getQuoteSection(item),
860
+ };
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,216 @@ 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)
411
958
  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))));
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)
971
+ return null;
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" }, this.renderAdmissionPrice(price), state.showScarcity && (h("span", { class: "admission-availability" }, admission.available, " tickets remaining")), !state.showScarcity && savings > 0 && (h("span", { class: "admission-savings" }, "Save ", this.formatPriceValue(savings))), state.label && (h("span", { class: `admission-sale-label ${state.label.tone}` }, state.label.text))), this.renderAdmissionControls(admission, sectionId, state, mode)));
985
+ }
986
+ renderAdmissionPrice(price) {
987
+ if (price.discounted) {
988
+ return (h("span", { class: "admission-price-row" }, h("span", { class: "admission-price discounted" }, this.formatPriceValue(price.total)), price.gross !== undefined && (h("span", { class: "admission-price-original" }, this.formatPriceValue(price.gross)))));
989
+ }
990
+ return (h("span", { class: "admission-price" }, this.formatPriceValue(price.total)));
413
991
  }
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")))));
992
+ renderAdmissionControls(admission, sectionId, state, mode) {
993
+ if (mode === "door" || state.postSale)
994
+ return null;
995
+ if (!state.soldOut) {
996
+ return this.renderQuantityControls(admission, sectionId, state.disabled, state.maxQuantity);
997
+ }
998
+ return (h("div", { class: "sold-out-controls" }, h("span", { class: "sold-out-text" }, "Sold Out")));
418
999
  }
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;
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 = this.admissionStatusText(admission, state.soldOut);
1015
+ return admissionAriaLabel(admission, section, status);
1016
+ }
1017
+ admissionStatusText(admission, soldOut) {
1018
+ if (soldOut)
1019
+ return "Sold Out";
1020
+ return this.formatPriceValue(priceTotal(admission.price));
1021
+ }
1022
+ renderAdmissionHeader(admission) {
1023
+ 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)));
1024
+ }
1025
+ renderMoreInfoButton(admission) {
1026
+ if (!admission.description)
438
1027
  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)))));
1028
+ 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")));
1029
+ }
1030
+ renderAdmissionBody(admission, event, sectionId, state, mode) {
1031
+ if (sectionId) {
1032
+ return this.renderPriceAndControls(admission, sectionId, state, mode);
1033
+ }
1034
+ return (h(h.Fragment, null, this.renderEventTimeAndLocation(event), this.renderPriceAndControls(admission, sectionId, state, mode)));
1035
+ }
1036
+ renderSectionHeader(section, admissions, isSectionSoldOut) {
1037
+ 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)));
1038
+ }
1039
+ renderSectionWithAdmissions(section, admissions, event, mode) {
1040
+ const soldOut = isSectionSoldOut({
1041
+ admissions,
1042
+ section,
1043
+ });
1044
+ 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
1045
  }
444
1046
  renderLoadingState() {
445
1047
  return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "loading-state" }, "Loading event details...")));
446
1048
  }
447
1049
  renderErrorState() {
1050
+ if (this.passcodeDenied)
1051
+ return this.renderPasscodeState();
448
1052
  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
1053
  }
1054
+ renderPasscodeState() {
1055
+ 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")))));
1056
+ }
450
1057
  renderNotFoundState() {
451
1058
  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
1059
  }
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
- })));
1060
+ renderCouponPanel() {
1061
+ const hasInput = this.couponInput.trim().length > 0;
1062
+ const busy = this.quoteLoading || this.previewLoading || this.checkoutLoading;
1063
+ const canApply = hasInput && !busy;
1064
+ 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
1065
  }
469
1066
  isCheckoutDisabled() {
470
1067
  return (this.quoteLoading ||
471
1068
  this.checkoutLoading ||
472
- !this.quote ||
473
- !this.quote.items?.length);
1069
+ Boolean(this.quoteError) ||
1070
+ !hasQuoteItems(this.quote));
474
1071
  }
475
1072
  getCheckoutButtonText() {
476
1073
  if (this.checkoutLoading)
477
1074
  return "Processing...";
1075
+ if (this.quote)
1076
+ return this.getQuoteCheckoutText();
1077
+ return "Pay";
1078
+ }
1079
+ renderQuoteLoadingContent() {
1080
+ return (h("span", { class: "checkout-spinner-wrapper", "aria-hidden": "true" }, LoaderCircleIcon()));
1081
+ }
1082
+ renderCheckoutButtonContent() {
478
1083
  if (this.quoteLoading)
479
- return "Updating...";
480
- return "Checkout";
1084
+ return this.renderQuoteLoadingContent();
1085
+ return this.getCheckoutButtonText();
1086
+ }
1087
+ isStickyCheckoutDisabled(state) {
1088
+ if (state.isDoorOnly)
1089
+ return false;
1090
+ return (state.isEnded ||
1091
+ state.isUnavailableForSale ||
1092
+ this.checkoutLoading ||
1093
+ this.quoteLoading);
1094
+ }
1095
+ getStickyCheckoutText(state) {
1096
+ if (this.checkoutLoading)
1097
+ return "Processing...";
1098
+ if (this.quote)
1099
+ return this.getQuoteCheckoutText();
1100
+ if (this.selectionPrompt)
1101
+ return "Select a ticket";
1102
+ if (state.isDoorOnly)
1103
+ return "Get Tickets at the Door";
1104
+ if (state.isEnded)
1105
+ return "Event Ended";
1106
+ if (state.isUnavailableForSale)
1107
+ return "Tickets Unavailable";
1108
+ return "Get Tickets";
481
1109
  }
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())));
1110
+ renderStickyCheckoutButtonContent(state) {
1111
+ if (this.quoteLoading)
1112
+ return this.renderQuoteLoadingContent();
1113
+ return this.getStickyCheckoutText(state);
1114
+ }
1115
+ getQuoteCheckoutText() {
1116
+ if (!this.quote)
1117
+ return "Pay";
1118
+ if (quoteTotal(this.quote) === 0)
1119
+ return "Continue";
1120
+ return `Pay ${this.getOrderTotalText()}`;
1121
+ }
1122
+ getOrderTotalText() {
1123
+ if (!this.quote)
1124
+ return "N/A";
1125
+ const total = quoteTotal(this.quote);
1126
+ if (total === 0)
1127
+ return "Free";
1128
+ return this.formatPriceValue(total);
1129
+ }
1130
+ renderCheckoutActions() {
1131
+ 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())));
1132
+ }
1133
+ renderStickyCheckout(state) {
1134
+ if (!this.stickyCheckout)
1135
+ return null;
1136
+ if (!state.showTicketing)
1137
+ return null;
1138
+ 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
1139
  }
489
1140
  renderModal() {
490
1141
  if (!this.openModalAdmissionId)
@@ -496,22 +1147,60 @@ export class SessionsTicketing {
496
1147
  // biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: backdrop click-to-close is a common modal pattern, keyboard handled via Escape key
497
1148
  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
1149
  }
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))));
1150
+ renderTicketSections(event) {
1151
+ const state = getTicketingState(event);
1152
+ const mode = state.isDoorOnly ? "door" : "default";
1153
+ const items = topLevelTicketItems(event.sections, visibleAdmissions(event, mode));
1154
+ 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
1155
  }
507
- render() {
508
- if (this.loading)
509
- return this.renderLoadingState();
510
- if (this.error && !this.event)
511
- return this.renderErrorState();
512
- if (!this.event)
1156
+ renderTopLevelItem(item, event, mode) {
1157
+ if (item.type === "section") {
1158
+ return this.renderSectionWithAdmissions(item.section, item.admissions, event, mode);
1159
+ }
1160
+ return (h("div", { class: "ticket-section", key: item.admission.id }, h("div", { class: "admissions-list" }, this.renderAdmission(item.admission, event, undefined, false, mode))));
1161
+ }
1162
+ renderDoorAlert(event, mode) {
1163
+ if (mode !== "door")
1164
+ return null;
1165
+ const location = event.location?.name?.trim() || "the venue";
1166
+ 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.")));
1167
+ }
1168
+ loadingView() {
1169
+ return this.loading ? this.renderLoadingState() : undefined;
1170
+ }
1171
+ errorView() {
1172
+ return this.error && !this.event ? this.renderErrorState() : undefined;
1173
+ }
1174
+ missingEventView() {
1175
+ return this.event ? undefined : this.renderNotFoundState();
1176
+ }
1177
+ pendingView() {
1178
+ return this.loadingView() ?? this.errorView() ?? this.missingEventView();
1179
+ }
1180
+ renderReadyState() {
1181
+ const event = this.event;
1182
+ if (!event)
513
1183
  return this.renderNotFoundState();
514
- return (h(Host, { "data-theme": this.currentTheme }, h("div", { class: "session-services-ticketing-container" }, this.renderTicketSections(), this.renderOrderSummary()), this.renderModal()));
1184
+ const state = getTicketingState(event);
1185
+ if (state.showExternalTicketing) {
1186
+ return this.renderExternalTicketing(state.externalTicketUrl);
1187
+ }
1188
+ return (h(Host, { "data-theme": this.currentTheme, "data-sticky-checkout": this.stickyCheckout ? "true" : undefined }, h("div", { class: "session-services-ticketing-container" }, state.showTicketing
1189
+ ? this.renderTicketSections(event)
1190
+ : this.renderUnavailableState(state), !state.isDoorOnly &&
1191
+ state.showTicketing &&
1192
+ this.renderCheckoutActions()), this.renderStickyCheckout(state), this.renderModal()));
1193
+ }
1194
+ renderExternalTicketing(url) {
1195
+ 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"))));
1196
+ }
1197
+ renderUnavailableState(state) {
1198
+ return (h("div", { class: "empty-ticketing-state" }, h("strong", null, state.isEnded ? "Event ended" : "Tickets unavailable"), h("span", null, state.isEnded
1199
+ ? "Ticket sales are closed for this event."
1200
+ : "Tickets are not currently available for this event.")));
1201
+ }
1202
+ render() {
1203
+ return this.pendingView() ?? this.renderReadyState();
515
1204
  }
516
1205
  static get is() { return "session-services-ticketing"; }
517
1206
  static get encapsulation() { return "shadow"; }
@@ -624,6 +1313,113 @@ export class SessionsTicketing {
624
1313
  "reflect": false,
625
1314
  "attribute": "return-url"
626
1315
  },
1316
+ "couponCode": {
1317
+ "type": "string",
1318
+ "mutable": false,
1319
+ "complexType": {
1320
+ "original": "string",
1321
+ "resolved": "string",
1322
+ "references": {}
1323
+ },
1324
+ "required": false,
1325
+ "optional": true,
1326
+ "docs": {
1327
+ "tags": [],
1328
+ "text": ""
1329
+ },
1330
+ "getter": false,
1331
+ "setter": false,
1332
+ "reflect": false,
1333
+ "attribute": "coupon-code"
1334
+ },
1335
+ "initialAdmissionPrices": {
1336
+ "type": "string",
1337
+ "mutable": false,
1338
+ "complexType": {
1339
+ "original": "| Record<string, { discount: number; gross: number; total: number }>\n | string",
1340
+ "resolved": "string | { [x: string]: { discount: number; gross: number; total: number; }; }",
1341
+ "references": {
1342
+ "Record": {
1343
+ "location": "global",
1344
+ "id": "global::Record"
1345
+ }
1346
+ }
1347
+ },
1348
+ "required": false,
1349
+ "optional": true,
1350
+ "docs": {
1351
+ "tags": [],
1352
+ "text": ""
1353
+ },
1354
+ "getter": false,
1355
+ "setter": false,
1356
+ "reflect": false,
1357
+ "attribute": "initial-admission-prices"
1358
+ },
1359
+ "initialPreviewQuote": {
1360
+ "type": "string",
1361
+ "mutable": false,
1362
+ "complexType": {
1363
+ "original": "OrderQuote | string",
1364
+ "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; }; }",
1365
+ "references": {
1366
+ "OrderQuote": {
1367
+ "location": "import",
1368
+ "path": "@session-services/sdk/schemas",
1369
+ "id": "../../sdk/src/schemas.ts::OrderQuote"
1370
+ }
1371
+ }
1372
+ },
1373
+ "required": false,
1374
+ "optional": true,
1375
+ "docs": {
1376
+ "tags": [],
1377
+ "text": ""
1378
+ },
1379
+ "getter": false,
1380
+ "setter": false,
1381
+ "reflect": false,
1382
+ "attribute": "initial-preview-quote"
1383
+ },
1384
+ "passcode": {
1385
+ "type": "string",
1386
+ "mutable": false,
1387
+ "complexType": {
1388
+ "original": "string",
1389
+ "resolved": "string",
1390
+ "references": {}
1391
+ },
1392
+ "required": false,
1393
+ "optional": true,
1394
+ "docs": {
1395
+ "tags": [],
1396
+ "text": ""
1397
+ },
1398
+ "getter": false,
1399
+ "setter": false,
1400
+ "reflect": false,
1401
+ "attribute": "passcode"
1402
+ },
1403
+ "stickyCheckout": {
1404
+ "type": "boolean",
1405
+ "mutable": false,
1406
+ "complexType": {
1407
+ "original": "boolean",
1408
+ "resolved": "boolean",
1409
+ "references": {}
1410
+ },
1411
+ "required": false,
1412
+ "optional": false,
1413
+ "docs": {
1414
+ "tags": [],
1415
+ "text": ""
1416
+ },
1417
+ "getter": false,
1418
+ "setter": false,
1419
+ "reflect": false,
1420
+ "attribute": "sticky-checkout",
1421
+ "defaultValue": "false"
1422
+ },
627
1423
  "theme": {
628
1424
  "type": "string",
629
1425
  "mutable": false,
@@ -691,10 +1487,19 @@ export class SessionsTicketing {
691
1487
  "event": {},
692
1488
  "selectedItems": {},
693
1489
  "quote": {},
1490
+ "previewQuote": {},
694
1491
  "loading": {},
695
1492
  "error": {},
696
1493
  "quoteLoading": {},
1494
+ "previewLoading": {},
697
1495
  "quoteError": {},
1496
+ "couponError": {},
1497
+ "couponInput": {},
1498
+ "appliedCouponCode": {},
1499
+ "passcodeInput": {},
1500
+ "activePasscode": {},
1501
+ "passcodeDenied": {},
1502
+ "selectionPrompt": {},
698
1503
  "checkoutLoading": {},
699
1504
  "currentTheme": {},
700
1505
  "openModalAdmissionId": {}
@@ -728,7 +1533,7 @@ export class SessionsTicketing {
728
1533
  },
729
1534
  "complexType": {
730
1535
  "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; }; }",
1536
+ "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
1537
  "references": {
733
1538
  "OrderQuote": {
734
1539
  "location": "import",
@@ -754,6 +1559,7 @@ export class SessionsTicketing {
754
1559
  }
755
1560
  }];
756
1561
  }
1562
+ static get elementRef() { return "host"; }
757
1563
  static get watchers() {
758
1564
  return [{
759
1565
  "propName": "tenantId",
@@ -764,6 +1570,18 @@ export class SessionsTicketing {
764
1570
  }, {
765
1571
  "propName": "apiUrl",
766
1572
  "methodName": "propsChanged"
1573
+ }, {
1574
+ "propName": "passcode",
1575
+ "methodName": "propsChanged"
1576
+ }, {
1577
+ "propName": "couponCode",
1578
+ "methodName": "couponCodeChanged"
1579
+ }, {
1580
+ "propName": "initialAdmissionPrices",
1581
+ "methodName": "couponCodeChanged"
1582
+ }, {
1583
+ "propName": "initialPreviewQuote",
1584
+ "methodName": "couponCodeChanged"
767
1585
  }, {
768
1586
  "propName": "theme",
769
1587
  "methodName": "themeChanged"