@ticketlayer/elements 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
  2. package/dist/cjs/index.cjs.js +2 -1
  3. package/dist/cjs/loader.cjs.js +2 -2
  4. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  5. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  6. package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
  7. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -74
  8. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
  9. package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
  10. package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
  11. package/dist/cjs/tl-cart.cjs.entry.js +71 -103
  12. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
  13. package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
  14. package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
  15. package/dist/cjs/tl-login.cjs.entry.js +74 -141
  16. package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
  17. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
  18. package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
  19. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
  20. package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
  21. package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
  22. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  23. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  24. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
  25. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
  26. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  27. package/dist/collection/components/tl-cart/tl-cart.js +82 -107
  28. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  29. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  30. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  31. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  32. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  33. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  34. package/dist/collection/components/tl-login/tl-login.js +83 -147
  35. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  36. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  37. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
  38. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  39. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  40. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  41. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
  42. package/dist/collection/utils/order-summary.js +15 -135
  43. package/dist/collection/utils/testing/deep-text.js +29 -0
  44. package/dist/collection/utils/testing/fixtures.js +290 -0
  45. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  46. package/dist/collection/utils/web-query.js +12 -0
  47. package/dist/collection/utils/web-storage.js +22 -0
  48. package/dist/components/ticket-selector.js +1 -0
  49. package/dist/components/tl-buy-modal.js +1 -1
  50. package/dist/components/tl-buy-tickets-button.js +1 -1
  51. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  52. package/dist/components/tl-cart-badge.js +1 -1
  53. package/dist/components/tl-cart-drawer.js +1 -1
  54. package/dist/components/tl-cart2.js +1 -1
  55. package/dist/components/tl-checkout-wrapper2.js +1 -1
  56. package/dist/components/tl-event-card2.js +1 -1
  57. package/dist/components/tl-event-list.js +1 -1
  58. package/dist/components/tl-login2.js +1 -1
  59. package/dist/components/tl-my-orders.js +1 -1
  60. package/dist/components/tl-occurrence-selector2.js +1 -1
  61. package/dist/components/tl-order-confirmation.js +1 -1
  62. package/dist/components/tl-order-tickets2.js +1 -1
  63. package/dist/components/tl-promo-code.js +1 -1
  64. package/dist/components/tl-seat-map.js +1 -1
  65. package/dist/components/tl-ticket-selector2.js +1 -1
  66. package/dist/components/web-query.js +1 -0
  67. package/dist/elements.docs.json +34 -34
  68. package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
  69. package/dist/esm/index.js +2 -1
  70. package/dist/esm/loader.js +3 -3
  71. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  72. package/dist/esm/ticketlayer-elements.js +3 -3
  73. package/dist/esm/tl-buy-modal.entry.js +3 -3
  74. package/dist/esm/tl-buy-tickets-button.entry.js +90 -75
  75. package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
  76. package/dist/esm/tl-cart-badge.entry.js +75 -62
  77. package/dist/esm/tl-cart-drawer.entry.js +52 -24
  78. package/dist/esm/tl-cart.entry.js +72 -104
  79. package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
  80. package/dist/esm/tl-event-card.entry.js +44 -106
  81. package/dist/esm/tl-event-list.entry.js +73 -36
  82. package/dist/esm/tl-login.entry.js +75 -142
  83. package/dist/esm/tl-my-orders.entry.js +53 -93
  84. package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
  85. package/dist/esm/tl-order-confirmation.entry.js +72 -108
  86. package/dist/esm/tl-order-tickets_2.entry.js +49 -75
  87. package/dist/esm/tl-promo-code.entry.js +51 -48
  88. package/dist/esm/tl-seat-map.entry.js +3 -3
  89. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  90. package/dist/ticketlayer-elements/index.esm.js +1 -1
  91. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  92. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  93. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  94. package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
  95. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  96. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  97. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  98. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  99. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  100. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  101. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  102. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  103. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  104. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  105. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  106. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  107. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  108. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  109. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  110. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  111. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  112. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  113. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  114. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
  115. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  116. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  117. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  118. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  119. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  120. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  121. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  122. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  123. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  124. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  125. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  126. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  127. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  128. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
  129. package/dist/types/components.d.ts +270 -62
  130. package/dist/types/utils/order-summary.d.ts +14 -97
  131. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  132. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  133. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  134. package/dist/types/utils/web-query.d.ts +18 -0
  135. package/dist/types/utils/web-storage.d.ts +16 -0
  136. package/elements.catalogue.json +602 -50
  137. package/package.json +8 -6
  138. package/dist/cjs/money-BtmM2gt-.js +0 -118
  139. package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
  140. package/dist/components/money.js +0 -1
  141. package/dist/components/order-summary.js +0 -1
  142. package/dist/esm/money-4ig0-WB6.js +0 -112
  143. package/dist/esm/order-summary-DiDtvfZM.js +0 -136
  144. package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
  145. package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
  146. package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
  147. package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
  148. package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
  149. package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
  150. package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
  151. package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
  152. package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
  153. package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
  154. package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
  155. package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
  156. package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
  157. package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
  158. package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
  159. package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
  160. package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
  161. package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { h } from "@stencil/core";
2
- import { onSDKEvent, whenSDK } from "../../utils/sdk-connector";
3
- import { errorMessage, formatDateTime, formatPrice, orderCustomerEmail, orderEventName, orderTotals, resolveOccurrence, ticketItems, } from "../../utils/order-summary";
2
+ import { createElement, orderConfirmationCore, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
4
+ import { webQuery } from "../../utils/web-query";
4
5
  /**
5
6
  * tl-order-confirmation - the "thank you" page block: order reference,
6
7
  * customer email, event and date, ticket lines, totals, then the tickets
@@ -13,6 +14,13 @@ import { errorMessage, formatDateTime, formatPrice, orderCustomerEmail, orderEve
13
14
  * 3. the SDK's `checkout:completed` event, for a page that mounts this before
14
15
  * the customer pays
15
16
  *
17
+ * The web harness for `orderConfirmationCore`: it renders and it forwards. All
18
+ * three of those routes, the money rows, the event lookup and every string are
19
+ * in `@ticketlayer/elements-core` (AGENTS.md, TKT-11). What is left here is the
20
+ * Stencil surface and the two genuinely web things: the client the page
21
+ * published on `window.Ticketlayer`, and `window.location.search`, which is
22
+ * where this runtime keeps the parameters the core asks for.
23
+ *
16
24
  * Usage: <tl-order-confirmation order-id="ord_xxx"></tl-order-confirmation>
17
25
  *
18
26
  * @event tlOrderLoaded - the order was loaded
@@ -24,127 +32,82 @@ export class TlOrderConfirmation {
24
32
  this.showTickets = true;
25
33
  /** Columns for the ticket grid. */
26
34
  this.columns = 2;
27
- this.order = null;
28
- this.resolvedOrderId = null;
29
- this.loading = false;
30
- this.error = null;
31
- this.paymentFailed = false;
32
- this.occurrence = null;
33
- this.signedInCustomer = null;
35
+ this.element = createElement(orderConfirmationCore, {
36
+ client: () => getSDK(),
37
+ clientReady: () => whenSDK(),
38
+ query: webQuery,
39
+ emit: (event, detail) => this.forward(event, detail),
40
+ });
41
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
42
+ this.view = this.element.view;
34
43
  }
35
- watchOrderId(next) {
36
- this.resolvedOrderId = next || this.orderIdFromUrl();
37
- this.load();
44
+ /**
45
+ * A new order id is a new subject, and the core resolves it again the way
46
+ * the mount did - the prop first, the link second. Not a remount: the
47
+ * `checkout:completed` subscription belongs to the element rather than to
48
+ * the order it is showing.
49
+ */
50
+ watchOrderId() {
51
+ this.syncProps();
52
+ this.element.actions.orderIdChanged();
38
53
  }
39
- async componentWillLoad() {
40
- const params = this.urlParams();
41
- // Stripe returns with ?redirect_status=; anything but succeeded is not paid.
42
- const redirect = params?.get('redirect_status');
43
- this.paymentFailed = !!redirect && redirect !== 'succeeded';
44
- this.resolvedOrderId = this.orderId || this.orderIdFromUrl();
45
- if (this.resolvedOrderId && !this.paymentFailed) {
46
- await this.load();
47
- }
48
- }
49
- componentDidLoad() {
50
- // A page that mounts before checkout: pick the order up when the SDK completes it.
51
- this.unsubscribe = onSDKEvent('checkout:completed', (payload) => {
52
- const order = payload;
53
- if (!order?.id)
54
- return;
55
- this.paymentFailed = false;
56
- this.order = order;
57
- this.resolvedOrderId = order.id;
58
- this.tlOrderLoaded.emit({ order });
59
- // Re-read for tickets, which are issued after the order is created.
60
- this.load();
54
+ syncProps() {
55
+ this.element.setProps({
56
+ orderId: this.orderId,
57
+ customerEmail: this.customerEmail,
58
+ showTickets: this.showTickets,
59
+ columns: this.columns,
61
60
  });
62
61
  }
62
+ connectedCallback() {
63
+ this.syncProps();
64
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
65
+ this.view = view;
66
+ }));
67
+ this.view = this.element.view;
68
+ this.teardown = this.element.mount();
69
+ }
70
+ /**
71
+ * Off screen: run the mount's teardown, which drops the
72
+ * `checkout:completed` subscription. Not `destroy()`: Stencil calls this for
73
+ * a DOM move as well as a removal, and a destroyed core never mounts again.
74
+ */
63
75
  disconnectedCallback() {
76
+ this.teardown?.();
77
+ this.teardown = undefined;
64
78
  this.unsubscribe?.();
79
+ this.unsubscribe = undefined;
65
80
  }
66
- urlParams() {
67
- if (typeof window === 'undefined' || !window.location?.search)
68
- return null;
69
- return new URLSearchParams(window.location.search);
81
+ forward(event, detail) {
82
+ if (event === 'tlOrderLoaded')
83
+ this.tlOrderLoaded.emit(detail);
84
+ else if (event === 'tlError')
85
+ this.tlError.emit(detail);
70
86
  }
71
- orderIdFromUrl() {
72
- return this.urlParams()?.get('orderId') || null;
87
+ renderRows() {
88
+ return (h("dl", { class: "rows" }, this.view.derived.rows.map((row) => (h("div", { class: "row", key: row.label }, h("dt", null, row.label), h("dd", { class: row.reference ? 'reference' : undefined }, row.value))))));
73
89
  }
74
- async load() {
75
- const id = this.resolvedOrderId;
76
- if (!id)
77
- return;
78
- try {
79
- this.loading = !this.order;
80
- this.error = null;
81
- const sdk = await whenSDK();
82
- const orders = sdk.getOrdersManager?.();
83
- if (!orders)
84
- throw new Error('Orders are not available');
85
- const order = (await orders.get(id));
86
- if (!order)
87
- throw new Error('Order not found');
88
- this.order = order;
89
- this.signedInCustomer = sdk.getAuthManager?.()?.customer ?? null;
90
- this.tlOrderLoaded.emit({ order });
91
- this.resolveEvent(sdk, order);
92
- }
93
- catch (err) {
94
- console.error('[tl-order-confirmation] Failed to load order:', err);
95
- this.error = errorMessage(err, 'Failed to load your order');
96
- this.tlError.emit({ message: this.error });
97
- }
98
- finally {
99
- this.loading = false;
100
- }
101
- }
102
- // The order view has no event name or date; look them up from the first
103
- // ticket's occurrence. Non-blocking and best effort.
104
- async resolveEvent(sdk, order) {
105
- const occurrenceId = order.tickets?.find((t) => t.eventOccurrenceId)?.eventOccurrenceId;
106
- if (!occurrenceId)
107
- return;
108
- try {
109
- const resolved = await resolveOccurrence(sdk, occurrenceId);
110
- if (resolved && this.order?.id === order.id)
111
- this.occurrence = resolved;
112
- }
113
- catch {
114
- // Leave the event line at what the order gives us.
115
- }
116
- }
117
- renderRows(order) {
118
- const email = this.customerEmail || orderCustomerEmail(order, this.signedInCustomer);
119
- const eventName = this.occurrence?.eventName || orderEventName(order);
120
- const startsAt = this.occurrence?.startsAt;
121
- return (h("dl", { class: "rows" }, h("div", { class: "row" }, h("dt", null, "Order reference"), h("dd", { class: "reference" }, order.orderNumber)), email && (h("div", { class: "row" }, h("dt", null, "Email"), h("dd", null, email))), eventName && (h("div", { class: "row" }, h("dt", null, "Event"), h("dd", null, eventName))), startsAt && (h("div", { class: "row" }, h("dt", null, "Date"), h("dd", null, formatDateTime(startsAt))))));
122
- }
123
- // Money comes from orderTotals: the booking fee is its own row named after
124
- // the channel's fee profile, the subtotal is the ticket lines alone (the
125
- // order's own `subtotal` folds the fee line into it) and the total is the
126
- // order's figure, so what is shown is what was charged.
127
- renderTotals(order) {
128
- const totals = orderTotals(order);
129
- const c = totals.currency;
130
- return (h("div", { class: "totals" }, h("div", { class: "total-row" }, h("span", null, "Subtotal"), h("span", null, formatPrice(totals.subtotal, c))), totals.showFee && (h("div", { class: "total-row fee" }, h("span", null, totals.feeLabel), h("span", null, formatPrice(totals.fees, c)))), totals.tax > 0 && (h("div", { class: "total-row" }, h("span", null, "Tax"), h("span", null, formatPrice(totals.tax, c)))), totals.discount > 0 && (h("div", { class: "total-row discount" }, h("span", null, "Discount"), h("span", null, "-", formatPrice(totals.discount, c)))), h("div", { class: "total-row total" }, h("span", null, "Total"), h("span", null, formatPrice(totals.total, c)))));
90
+ // Money comes from the core's orderTotals: the booking fee is its own row
91
+ // named after the channel's fee profile, the subtotal is the ticket lines
92
+ // alone (the order's own `subtotal` folds the fee line into it) and the
93
+ // total is the order's figure, so what is shown is what was charged.
94
+ renderTotals() {
95
+ const { strings, derived } = this.view;
96
+ return (h("div", { class: "totals" }, h("div", { class: "total-row" }, h("span", null, strings.subtotalRow), h("span", null, derived.subtotalLabel)), derived.totals.showFee && (h("div", { class: "total-row fee" }, h("span", null, derived.totals.feeLabel), h("span", null, derived.feeAmountLabel))), derived.showTaxRow && (h("div", { class: "total-row" }, h("span", null, strings.taxRow), h("span", null, derived.taxLabel))), derived.showDiscountRow && (h("div", { class: "total-row discount" }, h("span", null, strings.discountRow), h("span", null, "-", derived.discountLabel))), h("div", { class: "total-row total" }, h("span", null, strings.totalRow), h("span", null, derived.totalLabel))));
131
97
  }
132
98
  render() {
133
- if (this.paymentFailed) {
134
- return (h("div", { class: "confirmation" }, h("h1", { class: "title" }, "Payment not completed"), h("p", { class: "lead" }, "Your payment wasn't completed. Your cart is still saved, so you can try again.")));
99
+ const { status, state, strings, derived } = this.view;
100
+ if (status === 'payment-failed') {
101
+ return (h("div", { class: "confirmation" }, h("h1", { class: "title" }, strings.paymentFailedTitle), h("p", { class: "lead" }, strings.paymentFailedLead)));
135
102
  }
136
103
  // Nothing to show yet: no order in props, URL or from checkout.
137
- if (!this.resolvedOrderId && !this.order) {
104
+ if (status === 'idle') {
138
105
  return null;
139
106
  }
140
- if (this.loading) {
107
+ if (status === 'loading') {
141
108
  return (h("div", { class: "confirmation" }, h("div", { class: "skeleton-line short" }), h("div", { class: "skeleton-line long" }), h("div", { class: "skeleton-block" })));
142
109
  }
143
- const order = this.order;
144
- const orderId = order?.id || this.resolvedOrderId;
145
- return (h("div", { class: "confirmation" }, h("div", { class: "check", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" }))), h("h1", { class: "title" }, "You're going!"), h("p", { class: "lead" }, order?.orderNumber
146
- ? `Order ${order.orderNumber} is confirmed. Your tickets are on their way to your email.`
147
- : 'Your order is confirmed. Your tickets are on their way to your email.'), this.error && !order && (h("p", { class: "error" }, "We couldn't load your order details yet. ", this.error)), order && Array.isArray(order.items) && (h("div", { class: "summary" }, this.renderRows(order), h("ul", { class: "lines" }, ticketItems(order).map((item, i) => (h("li", { class: "line", key: i }, h("span", { class: "line-name" }, item.quantity, " x ", item.name), h("span", { class: "line-price" }, formatPrice(item.subtotal, order.currency)))))), this.renderTotals(order))), this.showTickets && order && (h("div", { class: "tickets" }, h("h2", { class: "subtitle" }, "Your tickets"), h("tl-order-tickets", { orderId: orderId, columns: this.columns }), h("p", { class: "hint" }, "Show these QR codes at the door for entry. Use Download PDF on a ticket to save it.")))));
110
+ return (h("div", { class: "confirmation" }, h("div", { class: "check", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" }))), h("h1", { class: "title" }, strings.title), h("p", { class: "lead" }, derived.leadText), derived.showError && (h("p", { class: "error" }, strings.errorPrefix, state.error)), derived.showSummary && (h("div", { class: "summary" }, this.renderRows(), h("ul", { class: "lines" }, derived.lines.map((line, i) => (h("li", { class: "line", key: i }, h("span", { class: "line-name" }, line.label), h("span", { class: "line-price" }, line.priceLabel))))), this.renderTotals())), derived.showTickets && (h("div", { class: "tickets" }, h("h2", { class: "subtitle" }, strings.ticketsTitle), h("tl-order-tickets", { orderId: derived.ticketsOrderId, columns: this.columns }), h("p", { class: "hint" }, strings.ticketsHint)))));
148
111
  }
149
112
  static get is() { return "tl-order-confirmation"; }
150
113
  static get encapsulation() { return "shadow"; }
@@ -242,13 +205,7 @@ export class TlOrderConfirmation {
242
205
  }
243
206
  static get states() {
244
207
  return {
245
- "order": {},
246
- "resolvedOrderId": {},
247
- "loading": {},
248
- "error": {},
249
- "paymentFailed": {},
250
- "occurrence": {},
251
- "signedInCustomer": {}
208
+ "view": {}
252
209
  };
253
210
  }
254
211
  static get events() {
@@ -268,8 +225,8 @@ export class TlOrderConfirmation {
268
225
  "references": {
269
226
  "TlOrderData": {
270
227
  "location": "import",
271
- "path": "../../utils/order-summary",
272
- "id": "src/utils/order-summary.ts::TlOrderData",
228
+ "path": "@ticketlayer/elements-core",
229
+ "id": "../elements-core/dist/esm/index.d.ts::TlOrderData",
273
230
  "referenceLocation": "TlOrderData"
274
231
  }
275
232
  }
@@ -295,6 +252,15 @@ export class TlOrderConfirmation {
295
252
  return [{
296
253
  "propName": "orderId",
297
254
  "methodName": "watchOrderId"
255
+ }, {
256
+ "propName": "customerEmail",
257
+ "methodName": "syncProps"
258
+ }, {
259
+ "propName": "showTickets",
260
+ "methodName": "syncProps"
261
+ }, {
262
+ "propName": "columns",
263
+ "methodName": "syncProps"
298
264
  }];
299
265
  }
300
266
  }
@@ -1,5 +1,6 @@
1
1
  import { h } from "@stencil/core";
2
- import { whenSDK } from "../../utils/sdk-connector";
2
+ import { createElement, orderTicketsCore, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
3
4
  /**
4
5
  * tl-order-tickets - renders all the tickets for an order as scannable QR
5
6
  * cards with PDF download. Fetches the order through the SDK (order-access
@@ -7,101 +8,73 @@ import { whenSDK } from "../../utils/sdk-connector";
7
8
  *
8
9
  * Usage: <tl-order-tickets order-id="ord_xxx"></tl-order-tickets>
9
10
  *
10
- * On an empty result it waits rather than concluding there is nothing.
11
- *
12
- * Tickets are issued asynchronously: the payment confirms, the order is
13
- * marked paid, and entitlement issuance follows a moment later. A confirmation
14
- * page therefore renders BEFORE the passes exist - reliably, not occasionally.
15
- * The component used to answer that with "No tickets for this order yet",
16
- * which is true and reads as "something went wrong" to someone who has just
17
- * been charged. Refreshing showed them, so the only thing missing was the
18
- * waiting.
19
- *
20
- * So: an empty order is treated as issuance in flight and polled, with the
21
- * gaps widening, for about a minute. Then it stops and says the tickets are
22
- * coming by email, because at that point something probably is wrong and a
23
- * page that polls forever is worse than one that admits it.
11
+ * On an empty result it waits rather than concluding there is nothing: tickets
12
+ * are issued a moment after the order is paid, so a confirmation page renders
13
+ * before the passes exist. The waiting, the widening schedule behind it and
14
+ * the point at which it gives up are all in `orderTicketsCore`
15
+ * (`@ticketlayer/elements-core`, AGENTS.md, TKT-11), along with every string.
16
+ * This harness renders and forwards, and the one thing it owns is pairing
17
+ * `mount()` with its teardown - which is what stops the polling when the
18
+ * element goes off screen.
24
19
  */
25
20
  export class TlOrderTickets {
26
21
  constructor() {
27
22
  /** Columns in the grid (default 2). */
28
23
  this.columns = 2;
29
- this.tickets = [];
30
- this.loading = true;
31
- this.error = null;
32
- /** The order loaded, but has no tickets yet and we are still waiting. */
33
- this.issuing = false;
34
- this.poll = 0;
24
+ this.element = createElement(orderTicketsCore, {
25
+ client: () => getSDK(),
26
+ clientReady: () => whenSDK(),
27
+ emit: (event, detail) => this.forward(event, detail),
28
+ });
29
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
30
+ this.view = this.element.view;
35
31
  }
32
+ /** A different order: forget this one and read again, from the top. */
36
33
  watchOrder() {
37
- this.reset();
38
- this.load();
34
+ this.syncProps();
35
+ this.element.actions.orderIdChanged();
39
36
  }
40
- async componentWillLoad() {
41
- await this.load();
42
- }
43
- disconnectedCallback() {
44
- this.stopPolling();
37
+ syncProps() {
38
+ this.element.setProps({ orderId: this.orderId, columns: this.columns });
45
39
  }
46
- reset() {
47
- this.stopPolling();
48
- this.poll = 0;
49
- this.tickets = [];
50
- this.issuing = false;
40
+ connectedCallback() {
41
+ this.syncProps();
42
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
43
+ this.view = view;
44
+ }));
45
+ this.view = this.element.view;
46
+ this.teardown = this.element.mount();
51
47
  }
52
- stopPolling() {
53
- if (this.timer !== undefined) {
54
- clearTimeout(this.timer);
55
- this.timer = undefined;
56
- }
48
+ /**
49
+ * Off screen: run the mount's teardown, which is what stops the polling.
50
+ * Not `destroy()`: Stencil calls this for a DOM move as well as a removal,
51
+ * and a destroyed core never mounts again.
52
+ */
53
+ disconnectedCallback() {
54
+ this.teardown?.();
55
+ this.teardown = undefined;
56
+ this.unsubscribe?.();
57
+ this.unsubscribe = undefined;
57
58
  }
58
- async load() {
59
- try {
60
- this.error = null;
61
- const sdk = await whenSDK();
62
- const orders = sdk.getOrdersManager?.();
63
- if (!orders)
64
- throw new Error('Tickets are not available');
65
- const order = (await orders.get(this.orderId));
66
- this.tickets = order?.tickets ?? [];
67
- if (this.tickets.length > 0) {
68
- // Arrived. Stop asking.
69
- this.issuing = false;
70
- this.stopPolling();
71
- }
72
- else {
73
- const delay = TlOrderTickets.POLL_DELAYS_MS[this.poll];
74
- // `undefined` once the schedule runs out: give up waiting, and say so.
75
- this.issuing = delay !== undefined;
76
- if (delay !== undefined) {
77
- this.poll += 1;
78
- this.stopPolling();
79
- this.timer = setTimeout(() => this.load(), delay);
80
- }
81
- }
82
- }
83
- catch (err) {
84
- // A failed request is a different thing from an order with no tickets,
85
- // and stops the polling: retrying a 403 a dozen times helps nobody.
86
- this.stopPolling();
87
- this.issuing = false;
88
- this.error = err instanceof Error ? err.message : 'Failed to load tickets';
89
- }
90
- finally {
91
- this.loading = false;
92
- }
59
+ /** The core declares no events, so nothing ever reaches here. */
60
+ forward(_event, _detail) {
61
+ // Intentionally empty: tl-order-tickets announces nothing to its host.
93
62
  }
94
63
  render() {
95
- if (this.loading) {
96
- return h("div", { class: "state" }, "Loading tickets\u2026");
64
+ const { status, state, strings, derived } = this.view;
65
+ if (status === 'loading') {
66
+ return h("div", { class: "state" }, strings.loading);
97
67
  }
98
- if (this.error) {
99
- return h("div", { class: "state error" }, this.error);
68
+ if (status === 'error') {
69
+ return h("div", { class: "state error" }, state.error);
100
70
  }
101
- if (this.tickets.length === 0) {
102
- return this.issuing ? (h("div", { class: "state issuing", role: "status", "aria-live": "polite" }, h("span", { class: "spinner", "aria-hidden": "true" }), "Preparing your tickets\u2026")) : (h("div", { class: "state", role: "status" }, "Your tickets are taking longer than usual. They will be emailed to you as soon as they are ready."));
71
+ if (status === 'issuing') {
72
+ return (h("div", { class: "state issuing", role: "status", "aria-live": "polite" }, h("span", { class: "spinner", "aria-hidden": "true" }), strings.preparing));
103
73
  }
104
- return (h("div", { class: `grid cols-${this.columns}` }, this.tickets.map((t) => (h("tl-ticket", { key: t.id, ticket: t, orderId: this.orderId })))));
74
+ if (status === 'delayed') {
75
+ return (h("div", { class: "state", role: "status" }, strings.delayed));
76
+ }
77
+ return (h("div", { class: `grid cols-${derived.columns}` }, derived.tickets.map((t) => (h("tl-ticket", { key: t.id, ticket: t, orderId: derived.orderId })))));
105
78
  }
106
79
  static get is() { return "tl-order-tickets"; }
107
80
  static get encapsulation() { return "shadow"; }
@@ -160,24 +133,16 @@ export class TlOrderTickets {
160
133
  }
161
134
  static get states() {
162
135
  return {
163
- "tickets": {},
164
- "loading": {},
165
- "error": {},
166
- "issuing": {}
136
+ "view": {}
167
137
  };
168
138
  }
169
139
  static get watchers() {
170
140
  return [{
171
141
  "propName": "orderId",
172
142
  "methodName": "watchOrder"
143
+ }, {
144
+ "propName": "columns",
145
+ "methodName": "syncProps"
173
146
  }];
174
147
  }
175
148
  }
176
- /**
177
- * Gaps between polls, in milliseconds. Front-loaded because issuance
178
- * usually lands in the first few seconds, then widening so a genuinely
179
- * stuck order is not hammered. Sums to roughly one minute.
180
- */
181
- TlOrderTickets.POLL_DELAYS_MS = [
182
- 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 12000,
183
- ];
@@ -1,6 +1,6 @@
1
1
  import { h } from "@stencil/core";
2
- import { whenSDK } from "../../utils/sdk-connector";
3
- import { errorMessage, isAuthError } from "../../utils/order-summary";
2
+ import { createElement, promoCodeCore, } from "@ticketlayer/elements-core";
3
+ import { getSDK, whenSDK } from "../../utils/sdk-connector";
4
4
  /**
5
5
  * tl-promo-code - an input plus Apply button that redeems a presale or promo
6
6
  * code for the current cart (POST /sales/presale-codes/redeem through the
@@ -10,6 +10,9 @@ import { errorMessage, isAuthError } from "../../utils/order-summary";
10
10
  * The API has no un-redeem, so the applied state offers only "use a different
11
11
  * code", which clears the input for another redemption.
12
12
  *
13
+ * The web harness for `promoCodeCore`: the redemption, the two error messages
14
+ * and every string are the core's (AGENTS.md, TKT-11).
15
+ *
13
16
  * Usage: <tl-promo-code></tl-promo-code>
14
17
  *
15
18
  * @event tlPromoApplied - a code was redeemed ({ code, benefit, alreadyHeld, cart })
@@ -21,59 +24,55 @@ export class TlPromoCode {
21
24
  this.placeholder = 'Promo or presale code';
22
25
  /** Apply button label. */
23
26
  this.buttonLabel = 'Apply';
24
- this.code = '';
25
- this.applying = false;
26
- this.applied = null;
27
- this.error = null;
27
+ this.element = createElement(promoCodeCore, {
28
+ client: () => getSDK(),
29
+ clientReady: () => whenSDK(),
30
+ emit: (event, detail) => this.forward(event, detail),
31
+ });
32
+ /** The core's whole view. Assigning it is what makes Stencil re-render. */
33
+ this.view = this.element.view;
28
34
  this.handleInput = (e) => {
29
- this.code = e.target.value;
30
- this.error = null;
35
+ this.element.actions.setCode(e.target.value);
31
36
  };
32
37
  this.handleSubmit = (e) => {
33
38
  e.preventDefault();
34
- this.apply();
35
- };
36
- this.reset = () => {
37
- this.applied = null;
38
- this.error = null;
39
+ this.element.actions.apply();
39
40
  };
41
+ this.reset = () => this.element.actions.reset();
40
42
  }
41
- async apply() {
42
- const code = this.code.trim();
43
- if (!code) {
44
- this.error = 'Enter a code';
45
- return;
46
- }
47
- this.applying = true;
48
- this.error = null;
49
- try {
50
- const sdk = await whenSDK();
51
- const presale = sdk.getPresaleManager?.();
52
- if (!presale)
53
- throw new Error('Promo codes are not available');
54
- const result = await presale.redeem(code);
55
- // The SDK refreshed the cart on redeem; read the cached copy for the payload.
56
- const cart = await sdk.getCartManager?.()?.get().catch(() => null) ?? null;
57
- this.applied = { code: code.toUpperCase(), ...result, cart };
58
- this.code = '';
59
- this.tlPromoApplied.emit(this.applied);
60
- }
61
- catch (err) {
62
- console.error('[tl-promo-code] Redeem failed:', err);
63
- this.error = isAuthError(err)
64
- ? 'Sign in to use this code'
65
- : errorMessage(err, 'That code could not be applied');
66
- this.tlError.emit({ message: this.error });
67
- }
68
- finally {
69
- this.applying = false;
70
- }
43
+ syncProps() {
44
+ this.element.setProps({ placeholder: this.placeholder, buttonLabel: this.buttonLabel });
45
+ }
46
+ /**
47
+ * This core has nothing to do on mount, and the harness calls `mount()`
48
+ * anyway: a harness never asks what a core's lifecycle means (TKT-197).
49
+ */
50
+ connectedCallback() {
51
+ this.syncProps();
52
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
53
+ this.view = view;
54
+ }));
55
+ this.view = this.element.view;
56
+ this.teardown = this.element.mount();
57
+ }
58
+ disconnectedCallback() {
59
+ this.teardown?.();
60
+ this.teardown = undefined;
61
+ this.unsubscribe?.();
62
+ this.unsubscribe = undefined;
63
+ }
64
+ forward(event, detail) {
65
+ if (event === 'tlPromoApplied')
66
+ this.tlPromoApplied.emit(detail);
67
+ else if (event === 'tlError')
68
+ this.tlError.emit(detail);
71
69
  }
72
70
  render() {
73
- if (this.applied) {
74
- return (h("div", { class: "applied", role: "status" }, h("span", { class: "applied-text" }, h("svg", { class: "icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "aria-hidden": "true" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), "Code ", h("strong", { class: "code" }, this.applied.code), this.applied.alreadyHeld ? ' was already applied' : ' applied'), h("button", { type: "button", class: "link", onClick: this.reset }, "Use a different code")));
71
+ const { status, state, strings, derived } = this.view;
72
+ if (status === 'applied') {
73
+ return (h("div", { class: "applied", role: "status" }, h("span", { class: "applied-text" }, h("svg", { class: "icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "aria-hidden": "true" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), strings.codePrefix, h("strong", { class: "code" }, derived.appliedCode), derived.appliedSuffix), h("button", { type: "button", class: "link", onClick: this.reset }, strings.useDifferent)));
75
74
  }
76
- return (h("form", { class: "promo", onSubmit: this.handleSubmit }, h("div", { class: "field" }, h("input", { class: "input", type: "text", autocomplete: "off", autoCapitalize: "characters", spellcheck: false, "aria-label": this.placeholder, placeholder: this.placeholder, value: this.code, disabled: this.applying, onInput: this.handleInput }), h("button", { type: "submit", class: "apply", disabled: this.applying }, this.applying ? 'Applying…' : this.buttonLabel)), this.error && (h("p", { class: "error", role: "alert" }, this.error))));
75
+ return (h("form", { class: "promo", onSubmit: this.handleSubmit }, h("div", { class: "field" }, h("input", { class: "input", type: "text", autocomplete: "off", autoCapitalize: "characters", spellcheck: false, "aria-label": this.placeholder, placeholder: this.placeholder, value: state.code, disabled: derived.disabled, onInput: this.handleInput }), h("button", { type: "submit", class: "apply", disabled: derived.disabled }, derived.actionLabel)), state.error && (h("p", { class: "error", role: "alert" }, state.error))));
77
76
  }
78
77
  static get is() { return "tl-promo-code"; }
79
78
  static get encapsulation() { return "shadow"; }
@@ -133,10 +132,7 @@ export class TlPromoCode {
133
132
  }
134
133
  static get states() {
135
134
  return {
136
- "code": {},
137
- "applying": {},
138
- "applied": {},
139
- "error": {}
135
+ "view": {}
140
136
  };
141
137
  }
142
138
  static get events() {
@@ -178,4 +174,13 @@ export class TlPromoCode {
178
174
  }
179
175
  }];
180
176
  }
177
+ static get watchers() {
178
+ return [{
179
+ "propName": "placeholder",
180
+ "methodName": "syncProps"
181
+ }, {
182
+ "propName": "buttonLabel",
183
+ "methodName": "syncProps"
184
+ }];
185
+ }
181
186
  }
@@ -132,10 +132,10 @@ export class TlSeatMap {
132
132
  // Render
133
133
  // -------------------------------------------------------------------------
134
134
  render() {
135
- return (h("div", { key: 'c9beadc1792d7825ac3f99e6abc8b181e28fd1b6', class: "seat-map-container", style: {
135
+ return (h("div", { key: '1c6a23b0aa021c58fa62c106a92a2e970ca7f6d1', class: "seat-map-container", style: {
136
136
  width: this.width,
137
137
  height: this.height,
138
- } }, this.isLoading && (h("div", { key: 'ad792ea35f433a268c81722ce4e7d0e34780d337', class: "loading" }, h("div", { key: 'dd3e0657b16c2f265f59fbaa2ac39c559c2093b7', class: "spinner" }), h("p", { key: '2714e3073d0e5cddc50d530aa1cf46a7f73ba96f' }, "Loading seat map..."))), this.error && (h("div", { key: '3de9d73ab89a1ee963e73b4192d5c93e9667f11b', class: "error" }, h("p", { key: '0fea5c828b5b212ffdb45c9c7702d241e2b79cc4', class: "error-title" }, "Error"), h("p", { key: 'f2c090b982bb1ee6fb7d347d8860b4ce4798a891', class: "error-message" }, this.error))), h("iframe", { key: '8023219fdd467ec736f9627190690a6f73df3bb3', ref: (el) => (this.iframeRef = el), src: this.getIframeSrc(), class: {
138
+ } }, this.isLoading && (h("div", { key: '4a09ddbdbd38eb68675bb320cf33fd3ead5974a4', class: "loading" }, h("div", { key: 'c622ba1f996427f0624cfda2c782abd49ec72e19', class: "spinner" }), h("p", { key: '802f42554fc238a1ddc0f8a0f6a6b0de89bdfb60' }, "Loading seat map..."))), this.error && (h("div", { key: 'dde73e2430bad68c0a67306c0aa2b44810007cc5', class: "error" }, h("p", { key: '392d51bbc47ab34eab5670aa7a2faf800dabe638', class: "error-title" }, "Error"), h("p", { key: 'fcf6810f767f34bf3490c1af21320d262bdb961b', class: "error-message" }, this.error))), h("iframe", { key: '2333206fa6eb4fe6cdcbb1d30864d56d54583e1f', ref: (el) => (this.iframeRef = el), src: this.getIframeSrc(), class: {
139
139
  'seat-map-iframe': true,
140
140
  hidden: this.isLoading || !!this.error,
141
141
  }, title: "Ticketlayer Seat Map", allow: "fullscreen" })));