@ticketlayer/elements-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +141 -4
  2. package/dist/cjs/cores/buy-tickets-button.js +213 -0
  3. package/dist/cjs/cores/buy-tickets-wrapper.js +106 -0
  4. package/dist/cjs/cores/cart-badge.js +150 -0
  5. package/dist/cjs/cores/cart-drawer.js +122 -0
  6. package/dist/cjs/cores/cart.js +247 -0
  7. package/dist/cjs/cores/checkout-wrapper.js +75 -0
  8. package/dist/cjs/cores/event-card.js +209 -0
  9. package/dist/cjs/cores/event-list.js +109 -0
  10. package/dist/cjs/cores/index.js +64 -1
  11. package/dist/cjs/cores/login.js +309 -0
  12. package/dist/cjs/cores/my-orders.js +221 -0
  13. package/dist/cjs/cores/occurrence-selector.js +528 -0
  14. package/dist/cjs/cores/order-confirmation.js +301 -0
  15. package/dist/cjs/cores/order-tickets.js +172 -0
  16. package/dist/cjs/cores/promo-code.js +136 -0
  17. package/dist/cjs/cores/ticket-selector.js +30 -0
  18. package/dist/cjs/define.js +76 -0
  19. package/dist/cjs/index.js +17 -1
  20. package/dist/cjs/manifest.js +5 -0
  21. package/dist/cjs/orders.js +169 -0
  22. package/dist/esm/client.d.ts +106 -0
  23. package/dist/esm/cores/buy-tickets-button.d.ts +55 -0
  24. package/dist/esm/cores/buy-tickets-button.js +210 -0
  25. package/dist/esm/cores/buy-tickets-wrapper.d.ts +27 -0
  26. package/dist/esm/cores/buy-tickets-wrapper.js +103 -0
  27. package/dist/esm/cores/cart-badge.d.ts +48 -0
  28. package/dist/esm/cores/cart-badge.js +147 -0
  29. package/dist/esm/cores/cart-drawer.d.ts +44 -0
  30. package/dist/esm/cores/cart-drawer.js +119 -0
  31. package/dist/esm/cores/cart.d.ts +105 -0
  32. package/dist/esm/cores/cart.js +244 -0
  33. package/dist/esm/cores/checkout-wrapper.d.ts +22 -0
  34. package/dist/esm/cores/checkout-wrapper.js +72 -0
  35. package/dist/esm/cores/event-card.d.ts +80 -0
  36. package/dist/esm/cores/event-card.js +205 -0
  37. package/dist/esm/cores/event-list.d.ts +35 -0
  38. package/dist/esm/cores/event-list.js +106 -0
  39. package/dist/esm/cores/index.d.ts +28 -0
  40. package/dist/esm/cores/index.js +42 -0
  41. package/dist/esm/cores/login.d.ts +125 -0
  42. package/dist/esm/cores/login.js +306 -0
  43. package/dist/esm/cores/my-orders.d.ts +63 -0
  44. package/dist/esm/cores/my-orders.js +218 -0
  45. package/dist/esm/cores/occurrence-selector.d.ts +170 -0
  46. package/dist/esm/cores/occurrence-selector.js +525 -0
  47. package/dist/esm/cores/order-confirmation.d.ts +99 -0
  48. package/dist/esm/cores/order-confirmation.js +298 -0
  49. package/dist/esm/cores/order-tickets.d.ts +46 -0
  50. package/dist/esm/cores/order-tickets.js +169 -0
  51. package/dist/esm/cores/promo-code.d.ts +56 -0
  52. package/dist/esm/cores/promo-code.js +132 -0
  53. package/dist/esm/cores/ticket-selector.js +30 -0
  54. package/dist/esm/define.d.ts +146 -0
  55. package/dist/esm/define.js +75 -0
  56. package/dist/esm/index.d.ts +5 -3
  57. package/dist/esm/index.js +2 -1
  58. package/dist/esm/manifest.d.ts +12 -0
  59. package/dist/esm/manifest.js +5 -0
  60. package/dist/esm/orders.d.ts +110 -0
  61. package/dist/esm/orders.js +154 -0
  62. package/package.json +2 -2
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `tl-promo-code`: an input and an Apply button that redeems a presale or
3
+ * promo code for the current cart.
4
+ *
5
+ * Redemption unlocks gated pricing on the customer; the client re-reads the
6
+ * cart and publishes `cart:updated`, so `tl-cart` and the badges re-render on
7
+ * their own and this element subscribes to nothing. It has nothing to do when
8
+ * a harness mounts it either, so it declares no `mount` hook - the harness
9
+ * still calls `mount()` and still gets a teardown, which is the point of the
10
+ * hook being unconditional.
11
+ *
12
+ * The API has no un-redeem, so the applied state offers only "use a different
13
+ * code", which clears the input for another redemption.
14
+ *
15
+ * Behaviour is what the Stencil component did before the split, to the letter,
16
+ * including the two error messages and the uppercased code in the receipt. What
17
+ * did not come across is the `console.error` tracing.
18
+ */
19
+ import { defineElement } from '../define.js';
20
+ const STRINGS = {
21
+ applying: 'Applying…',
22
+ codePrefix: 'Code ',
23
+ appliedSuffix: ' applied',
24
+ alreadyHeldSuffix: ' was already applied',
25
+ useDifferent: 'Use a different code',
26
+ enterCode: 'Enter a code',
27
+ notAvailable: 'Promo codes are not available',
28
+ signIn: 'Sign in to use this code',
29
+ rejected: 'That code could not be applied',
30
+ };
31
+ /** True for the "not signed in" family of API errors (401/403, NO_SESSION, NOT_AUTHENTICATED). */
32
+ export function isAuthError(err) {
33
+ const e = err;
34
+ if (!e || typeof e !== 'object')
35
+ return false;
36
+ return e.status === 401 || e.status === 403 || e.code === 'NO_SESSION' || e.code === 'NOT_AUTHENTICATED';
37
+ }
38
+ export const promoCodeCore = defineElement({
39
+ tag: 'tl-promo-code',
40
+ docs: 'Redeem a presale or promo code for the current cart.',
41
+ category: 'purchase',
42
+ runtimes: ['web'],
43
+ tokens: [
44
+ 'background',
45
+ 'border',
46
+ 'error',
47
+ 'font-family',
48
+ 'font-family-mono',
49
+ 'foreground',
50
+ 'muted',
51
+ 'muted-foreground',
52
+ 'primary',
53
+ 'primary-foreground',
54
+ 'radius-md',
55
+ 'spacing-2',
56
+ 'spacing-3',
57
+ 'spacing-4',
58
+ 'success',
59
+ ],
60
+ sdkMethods: ['cart.get', 'presale.redeem'],
61
+ props: {
62
+ placeholder: {
63
+ type: 'string',
64
+ attr: 'placeholder',
65
+ default: 'Promo or presale code',
66
+ docs: 'Input placeholder.',
67
+ },
68
+ buttonLabel: { type: 'string', attr: 'button-label', default: 'Apply', docs: 'Apply button label.' },
69
+ },
70
+ events: {
71
+ tlPromoApplied: { detail: 'TlPromoApplied', docs: '' },
72
+ tlError: { detail: '{ message: string; }', docs: '' },
73
+ },
74
+ strings: STRINGS,
75
+ statuses: ['idle', 'applied'],
76
+ state: () => ({ code: '', applying: false, applied: null, error: null }),
77
+ status: ({ state }) => (state.applied ? 'applied' : 'idle'),
78
+ derive: ({ state, props, strings }) => ({
79
+ actionLabel: state.applying ? strings.applying : props.buttonLabel,
80
+ disabled: state.applying,
81
+ appliedCode: state.applied?.code ?? null,
82
+ appliedSuffix: state.applied?.alreadyHeld ? strings.alreadyHeldSuffix : strings.appliedSuffix,
83
+ }),
84
+ actions: (ctx) => ({
85
+ /** The buyer typed. Typing clears whatever the last attempt complained about. */
86
+ setCode(code) {
87
+ ctx.setState({ code, error: null });
88
+ },
89
+ /**
90
+ * Redeem what is in the box.
91
+ *
92
+ * The client refreshes the cart on redeem, so the cart is read back only
93
+ * to put it in the payload; a read that fails is not a failed redemption
94
+ * and does not become one.
95
+ */
96
+ async apply() {
97
+ const code = ctx.state.code.trim();
98
+ if (!code) {
99
+ ctx.setState({ error: ctx.strings.enterCode });
100
+ return;
101
+ }
102
+ ctx.setState({ applying: true, error: null });
103
+ try {
104
+ const client = await ctx.whenClient();
105
+ const presale = client.getPresaleManager?.();
106
+ if (!presale)
107
+ throw new Error(ctx.strings.notAvailable);
108
+ const result = await presale.redeem(code);
109
+ const cart = (await client.getCartManager?.()?.get?.().catch(() => null)) ?? null;
110
+ const applied = { code: code.toUpperCase(), ...result, cart };
111
+ ctx.setState({ applied, code: '' });
112
+ ctx.emit('tlPromoApplied', applied);
113
+ }
114
+ catch (err) {
115
+ const message = isAuthError(err)
116
+ ? ctx.strings.signIn
117
+ : err instanceof Error && err.message
118
+ ? err.message
119
+ : ctx.strings.rejected;
120
+ ctx.setState({ error: message });
121
+ ctx.emit('tlError', { message });
122
+ }
123
+ finally {
124
+ ctx.setState({ applying: false });
125
+ }
126
+ },
127
+ /** Back to the input, for a second code. There is no un-redeem to offer. */
128
+ reset() {
129
+ ctx.setState({ applied: null, error: null });
130
+ },
131
+ }),
132
+ });
@@ -92,6 +92,36 @@ export const ticketSelectorCore = defineElement({
92
92
  addSuccess: false,
93
93
  feeQuote: null,
94
94
  }),
95
+ /**
96
+ * On screen: wait for the host's client, then read the ticket types. The
97
+ * harness used to do this (it awaited `whenSDK()` and called `load()`
98
+ * itself); the core does it now, so every runtime loads at the same moment
99
+ * and in the same order (TKT-197).
100
+ *
101
+ * There is nothing to undo, so nothing is returned. The guards are what make
102
+ * a remount safe: `active()` is false once this mount has been torn down, so
103
+ * an element that was removed while the client was still coming up neither
104
+ * loads nor writes an error into a view nobody is rendering.
105
+ */
106
+ mount: (ctx) => {
107
+ // Nothing to read until the host has said which occurrence is on sale.
108
+ // Until then the element stays in `loading`, which is what it did before.
109
+ if (!ctx.props.eventId || !ctx.props.occurrenceId)
110
+ return;
111
+ void (async () => {
112
+ try {
113
+ await ctx.whenClient();
114
+ if (!ctx.active())
115
+ return;
116
+ await ctx.actions.load();
117
+ }
118
+ catch (err) {
119
+ if (!ctx.active())
120
+ return;
121
+ ctx.actions.connectFailed(err instanceof Error ? err.message : ctx.strings.noClient);
122
+ }
123
+ })();
124
+ },
95
125
  status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
96
126
  derive: (ctx) => {
97
127
  const { state, strings, props, formatMoney } = ctx;
@@ -33,6 +33,16 @@ export type PropType = 'string' | 'number' | 'boolean' | 'object' | 'array';
33
33
  */
34
34
  export interface PropSpec<V> {
35
35
  type: PropType;
36
+ /**
37
+ * The type exactly as the harness writes it, when that is narrower than
38
+ * `type`: a literal union (`'"lg" | "md" | "sm"'`) or an optional prop
39
+ * (`'string | undefined'`). Written out for the same reason
40
+ * {@link EventSpec.detail} is - the catalogue is JSON and a type is not -
41
+ * and checked against the Stencil build by the catalogue emitter, so a core
42
+ * and its harness cannot disagree about what a prop accepts. Left out when
43
+ * `type` says it all.
44
+ */
45
+ tsType?: string;
36
46
  attr: string | null;
37
47
  default?: V;
38
48
  required?: boolean;
@@ -74,6 +84,92 @@ export interface ActionContext<P extends object, S extends object, Str extends S
74
84
  optionalClient(): ElementClient | null;
75
85
  /** A timer the instance clears on destroy. */
76
86
  after(ms: number, run: () => void): void;
87
+ /**
88
+ * The client, once the host has published one. Rejects when the host never
89
+ * produces one, which on the web is a page that never started Live. A
90
+ * harness supplies the waiting through `clientReady`; with none it settles
91
+ * from whatever the client resolver says right now.
92
+ *
93
+ * On an action as well as a mount, because an action a buyer triggers can be
94
+ * the first thing that needs the client (`tl-promo-code` redeems a code
95
+ * without reading anything first). `client()` would throw on a page that is
96
+ * still starting up; this waits, which is what the components did.
97
+ */
98
+ whenClient(): Promise<ElementClient>;
99
+ /**
100
+ * The runtime's key/value store, for the little a core is allowed to
101
+ * remember between visits (`tl-cart-badge` keeps the last item count so the
102
+ * badge does not flash empty on the next page).
103
+ *
104
+ * A seam and not a direct `localStorage`, for the same reason `client` is
105
+ * one: a core has no runtime in it, and the store is not the same object on
106
+ * every runtime. A harness that has one hands it over; with none, reads
107
+ * return null and writes go nowhere, so a core never has to ask whether it
108
+ * has somewhere to write.
109
+ */
110
+ readonly storage: ElementStorage;
111
+ /**
112
+ * The parameters the host was opened with, for the elements a link lands on.
113
+ *
114
+ * The second seam, and the same bargain as {@link ActionContext.storage}: two
115
+ * elements are the far end of a link someone follows out of an email or back
116
+ * from a payment provider, and what they have to read is in that link.
117
+ * `tl-login` exchanges `?code=`, and `tl-order-confirmation` finds its order
118
+ * in `?orderId=` and learns from `?redirect_status=` whether the payment
119
+ * actually went through.
120
+ *
121
+ * Where those parameters live is per runtime and nowhere near a core: the
122
+ * web has `window.location.search`, and a native harness has whatever its
123
+ * deep-link router hands it. So the core says WHICH parameter it reads and
124
+ * WHEN, and the harness says WHERE they come from. A harness that has none
125
+ * passes none, every read comes back null, and the core falls through to the
126
+ * behaviour it has when a page carries no link - which for both of these is
127
+ * the behaviour a host who set the prop already gets.
128
+ */
129
+ readonly query: ElementQuery;
130
+ }
131
+ /**
132
+ * What a harness hands over for {@link ActionContext.storage}: strings in,
133
+ * strings out, synchronous, and allowed to fail silently. Deliberately the
134
+ * smallest thing every runtime can supply.
135
+ */
136
+ export interface ElementStorage {
137
+ get(key: string): string | null;
138
+ set(key: string, value: string): void;
139
+ }
140
+ /** The store a core gets when its harness supplies none: it forgets. */
141
+ export declare const NO_STORAGE: ElementStorage;
142
+ /**
143
+ * What a harness hands over for {@link ActionContext.query}: one named
144
+ * parameter at a time, synchronous, null when it is not there. Read-only on
145
+ * purpose - a core reports what a link said, it does not rewrite the address
146
+ * bar, which is a navigation decision and therefore the host's.
147
+ */
148
+ export interface ElementQuery {
149
+ get(name: string): string | null;
150
+ }
151
+ /** The parameters a core gets when its harness supplies none: there are none. */
152
+ export declare const NO_QUERY: ElementQuery;
153
+ /** What a mount teardown is: run it once, and running it again does nothing. */
154
+ export type MountTeardown = () => void;
155
+ /**
156
+ * What a mount is handed: everything an action gets, plus the actions
157
+ * themselves, a way to register cleanups, and the two things a subscription
158
+ * needs to be safe - the client once the host has published it, and whether
159
+ * this mount is still the live one.
160
+ */
161
+ export interface MountContext<P extends object, S extends object, Str extends StringMap, A extends ActionMap> extends ActionContext<P, S, Str> {
162
+ /** The core's own actions, so a mount can `actions.load()`. */
163
+ readonly actions: A;
164
+ /**
165
+ * Register a cleanup for this mount. Everything registered runs once, in
166
+ * reverse order, when the mount is torn down. A cleanup registered after
167
+ * this mount has already been torn down runs immediately, which is what
168
+ * makes an `await` before a subscription safe.
169
+ */
170
+ onTeardown(cleanup: () => void): void;
171
+ /** False once this mount has been torn down, or the instance destroyed. */
172
+ active(): boolean;
77
173
  }
78
174
  /** A core, as `defineElement` returns it. */
79
175
  export interface ElementCore<P extends object, S extends object, D extends object, A extends ActionMap, Status extends string, Str extends StringMap> {
@@ -99,6 +195,19 @@ export interface ElementCore<P extends object, S extends object, D extends objec
99
195
  /** Every value `status` can return; a harness renders one branch per entry. */
100
196
  readonly statuses: readonly Status[];
101
197
  readonly state: (props: Readonly<P>) => S;
198
+ /**
199
+ * What happens when a harness puts the element on screen, and what undoes
200
+ * it. Typically `actions.load()` plus any subscription the element lives on;
201
+ * the returned function, and anything registered with `ctx.onTeardown`, is
202
+ * what the harness runs when the element goes away again.
203
+ *
204
+ * The core owns this, not the harness (TKT-197): a harness that decided when
205
+ * to load or who owns a subscription would have to decide it again for every
206
+ * runtime, and the two would drift. A core with nothing to do on mount leaves
207
+ * it out; `instance.mount()` still exists and still returns a teardown, so a
208
+ * harness calls it unconditionally.
209
+ */
210
+ readonly mount?: (ctx: MountContext<P, S, Str, A>) => MountTeardown | void;
102
211
  readonly status: (ctx: ViewContext<P, S, Str>) => Status;
103
212
  readonly derive: (ctx: ViewContext<P, S, Str>) => D;
104
213
  readonly actions: (ctx: ActionContext<P, S, Str>) => A;
@@ -124,10 +233,29 @@ export interface CreateElementOptions<P extends object, Str extends StringMap> {
124
233
  emit?: (event: string, detail: unknown) => void;
125
234
  /** Copy overrides, per instance. */
126
235
  strings?: Partial<Str>;
236
+ /**
237
+ * Resolves when the host's client is available, for a harness whose runtime
238
+ * publishes one late. The web harness passes `whenSDK`; a harness whose
239
+ * client is there from the start passes nothing. Only `mount`'s
240
+ * `whenClient()` waits on it.
241
+ */
242
+ clientReady?: () => Promise<unknown>;
127
243
  /** The locale money is formatted in. */
128
244
  locale?: string;
129
245
  /** The currency used when a value carries none. */
130
246
  currency?: string;
247
+ /**
248
+ * Where the core may remember a little between visits. The web harness
249
+ * passes a `localStorage` wrapper; a harness with no store passes nothing
250
+ * and the core's reads come back null.
251
+ */
252
+ storage?: ElementStorage;
253
+ /**
254
+ * The parameters the host was opened with. The web harness passes a wrapper
255
+ * over `window.location.search`; a harness whose runtime has no such thing
256
+ * passes nothing and the core's reads come back null.
257
+ */
258
+ query?: ElementQuery;
131
259
  }
132
260
  export interface ElementInstance<P extends object, S extends object, D extends object, A extends ActionMap, Status extends string, Str extends StringMap> {
133
261
  readonly core: ElementCore<P, S, D, A, Status, Str>;
@@ -138,6 +266,24 @@ export interface ElementInstance<P extends object, S extends object, D extends o
138
266
  setProps(next: Partial<P>): void;
139
267
  setClient(client: ElementClient | null | (() => ElementClient | null)): void;
140
268
  subscribe(listener: (view: ElementView<P, S, D, Status, Str>) => void): () => void;
269
+ /**
270
+ * Put the element to work, and hand back what stops it again.
271
+ *
272
+ * A harness calls this when the element appears (`connectedCallback` on the
273
+ * web) and the returned teardown when it goes away. The rules, which
274
+ * `test/mount.test.mjs` holds to:
275
+ *
276
+ * - The teardown is idempotent: calling it twice does nothing the second
277
+ * time, and a teardown from an earlier mount does nothing at all.
278
+ * - At most one mount is live. Mounting while mounted tears the previous one
279
+ * down first, so a remount cannot leave two subscriptions behind.
280
+ * - `destroy()` is terminal and runs the live mount's teardown first, so a
281
+ * harness that only destroys still leaks nothing. Mounting a destroyed
282
+ * instance does nothing and returns a teardown that does nothing.
283
+ */
284
+ mount(): MountTeardown;
285
+ /** Whether a mount is live right now. */
286
+ readonly mounted: boolean;
141
287
  destroy(): void;
142
288
  }
143
289
  /**
@@ -1,3 +1,12 @@
1
+ /** The store a core gets when its harness supplies none: it forgets. */
2
+ export const NO_STORAGE = {
3
+ get: () => null,
4
+ set: () => undefined,
5
+ };
6
+ /** The parameters a core gets when its harness supplies none: there are none. */
7
+ export const NO_QUERY = {
8
+ get: () => null,
9
+ };
1
10
  const CATEGORIES = [
2
11
  'discovery',
3
12
  'purchase',
@@ -67,6 +76,8 @@ export function createElement(core, options = {}) {
67
76
  style: 'currency',
68
77
  currency: currency || fallbackCurrency,
69
78
  }).format(amountInMinorUnits / 100);
79
+ const storage = options.storage ?? NO_STORAGE;
80
+ const query = options.query ?? NO_QUERY;
70
81
  const viewContext = () => ({ props, state, strings, formatMoney });
71
82
  const buildView = () => {
72
83
  const ctx = viewContext();
@@ -85,6 +96,15 @@ export function createElement(core, options = {}) {
85
96
  for (const listener of [...listeners])
86
97
  listener(view);
87
98
  };
99
+ const whenClient = async () => {
100
+ if (options.clientReady)
101
+ await options.clientReady();
102
+ const client = resolveClient();
103
+ if (!client) {
104
+ throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
105
+ }
106
+ return client;
107
+ };
88
108
  const actionContext = {
89
109
  get props() {
90
110
  return props;
@@ -122,6 +142,9 @@ export function createElement(core, options = {}) {
122
142
  return null;
123
143
  }
124
144
  },
145
+ storage,
146
+ query,
147
+ whenClient,
125
148
  after(ms, run) {
126
149
  if (destroyed)
127
150
  return;
@@ -134,6 +157,52 @@ export function createElement(core, options = {}) {
134
157
  },
135
158
  };
136
159
  const actions = core.actions(actionContext);
160
+ // The live mount, as an identity rather than a boolean: a teardown closes
161
+ // over the token it was made for, so a stale one tears nothing down.
162
+ let mountToken = null;
163
+ let cleanups = [];
164
+ const teardownMount = (token) => {
165
+ if (mountToken !== token)
166
+ return;
167
+ mountToken = null;
168
+ const pending = cleanups;
169
+ cleanups = [];
170
+ for (const cleanup of pending.reverse())
171
+ cleanup();
172
+ };
173
+ const mount = () => {
174
+ if (destroyed)
175
+ return () => { };
176
+ if (mountToken)
177
+ teardownMount(mountToken);
178
+ const token = {};
179
+ mountToken = token;
180
+ const live = () => mountToken === token && !destroyed;
181
+ const returned = core.mount?.({
182
+ ...actionContext,
183
+ get props() {
184
+ return props;
185
+ },
186
+ get state() {
187
+ return state;
188
+ },
189
+ actions,
190
+ active: live,
191
+ onTeardown(cleanup) {
192
+ if (live())
193
+ cleanups.push(cleanup);
194
+ else
195
+ cleanup();
196
+ },
197
+ });
198
+ if (typeof returned === 'function') {
199
+ if (live())
200
+ cleanups.push(returned);
201
+ else
202
+ returned();
203
+ }
204
+ return () => teardownMount(token);
205
+ };
137
206
  return {
138
207
  core,
139
208
  get props() {
@@ -161,7 +230,13 @@ export function createElement(core, options = {}) {
161
230
  listeners.delete(listener);
162
231
  };
163
232
  },
233
+ mount,
234
+ get mounted() {
235
+ return mountToken !== null;
236
+ },
164
237
  destroy() {
238
+ if (mountToken)
239
+ teardownMount(mountToken);
165
240
  destroyed = true;
166
241
  for (const handle of timers)
167
242
  clearTimeout(handle);
@@ -13,11 +13,13 @@
13
13
  * what the docs site and a native host read is a projection of the object that
14
14
  * runs, not a second copy kept by hand.
15
15
  */
16
- export { createElement, defineElement } from './define.js';
17
- export type { ActionContext, ActionMap, CreateElementOptions, ElementCategory, ElementCore, ElementDefinition, ElementInstance, ElementView, EventSpec, PropSpec, PropSpecs, PropType, Runtime, StringMap, ViewContext, } from './define.js';
16
+ export { createElement, defineElement, NO_QUERY, NO_STORAGE } from './define.js';
17
+ export type { ActionContext, ActionMap, CreateElementOptions, ElementCategory, ElementCore, ElementDefinition, ElementInstance, ElementQuery, ElementStorage, ElementView, EventSpec, MountContext, MountTeardown, PropSpec, PropSpecs, PropType, Runtime, StringMap, ViewContext, } from './define.js';
18
18
  export { manifestOf } from './manifest.js';
19
19
  export type { AnyElementCore, ElementManifest, ManifestEvent, ManifestProp } from './manifest.js';
20
- export type { ElementCartManager, ElementClient, ElementEventsManager } from './client.js';
20
+ export type { ElementAuthManager, ElementCartManager, ElementCheckoutContext, ElementClient, ElementEventContext, ElementEventsManager, ElementModalOptions, ElementOrderResult, ElementOrdersManager, ElementPresaleManager, ElementPresaleRedemption, } from './client.js';
21
21
  export { cartTotals, expressesFee, minorUnits, selectionTotals, FEE_LABEL_FALLBACK, } from './money.js';
22
22
  export type { TlCartLike, TlFeeQuote, TlSelectionLine, TlSelectionTotals, TlTotals, } from './money.js';
23
+ export { errorMessage, formatDate, formatDateTime, formatPrice, orderCustomerEmail, orderEventName, orderTotals, resolveOccurrence, statusLabel, statusTone, ticketCount, ticketItems, totalsAgree, } from './orders.js';
24
+ export type { StatusTone, TlOrderData, TlOrderItem, TlOrderTicketRef, TlOrderTotals, TlResolvedOccurrence, TlTicketData, } from './orders.js';
23
25
  export * from './cores/index.js';
package/dist/esm/index.js CHANGED
@@ -13,7 +13,8 @@
13
13
  * what the docs site and a native host read is a projection of the object that
14
14
  * runs, not a second copy kept by hand.
15
15
  */
16
- export { createElement, defineElement } from './define.js';
16
+ export { createElement, defineElement, NO_QUERY, NO_STORAGE } from './define.js';
17
17
  export { manifestOf } from './manifest.js';
18
18
  export { cartTotals, expressesFee, minorUnits, selectionTotals, FEE_LABEL_FALLBACK, } from './money.js';
19
+ export { errorMessage, formatDate, formatDateTime, formatPrice, orderCustomerEmail, orderEventName, orderTotals, resolveOccurrence, statusLabel, statusTone, ticketCount, ticketItems, totalsAgree, } from './orders.js';
19
20
  export * from './cores/index.js';
@@ -18,6 +18,12 @@ export interface ManifestProp {
18
18
  name: string;
19
19
  attr: string | null;
20
20
  type: string;
21
+ /**
22
+ * The narrower type the core writes out, when it has one: a literal union or
23
+ * an optional prop. Null when `type` says it all. The catalogue emitter
24
+ * compares this, falling back to `type`, against the Stencil build.
25
+ */
26
+ tsType: string | null;
21
27
  default: unknown;
22
28
  required: boolean;
23
29
  docs: string;
@@ -41,6 +47,12 @@ export interface ElementManifest {
41
47
  strings: Record<string, string>;
42
48
  statuses: string[];
43
49
  actions: string[];
50
+ /**
51
+ * Whether the core declares a `mount` hook (TKT-197). A harness calls
52
+ * `instance.mount()` either way; this says whether anything happens, which
53
+ * is the one part of the lifecycle a consumer of the catalogue can see.
54
+ */
55
+ mounts: boolean;
44
56
  }
45
57
  /**
46
58
  * A core with its type parameters forgotten, which is all a registry or the
@@ -5,6 +5,7 @@ export function manifestOf(core) {
5
5
  name,
6
6
  attr: spec.attr,
7
7
  type: spec.type,
8
+ tsType: spec.tsType ?? null,
8
9
  default: spec.default ?? null,
9
10
  required: !!spec.required,
10
11
  docs: spec.docs,
@@ -25,6 +26,7 @@ export function manifestOf(core) {
25
26
  strings: { ...core.strings },
26
27
  statuses: [...core.statuses],
27
28
  actions: Object.keys(dryRunActions(core)).sort(),
29
+ mounts: typeof core.mount === 'function',
28
30
  };
29
31
  }
30
32
  /**
@@ -47,5 +49,8 @@ function dryRunActions(core) {
47
49
  client: refuse('client'),
48
50
  optionalClient: refuse('optionalClient'),
49
51
  after: refuse('after'),
52
+ storage: { get: refuse('storage.get'), set: refuse('storage.set') },
53
+ query: { get: refuse('query.get') },
54
+ whenClient: refuse('whenClient'),
50
55
  });
51
56
  }
@@ -0,0 +1,110 @@
1
+ import type { ElementClient } from './client.js';
2
+ export interface TlOrderItem {
3
+ name: string;
4
+ description?: string | null;
5
+ type?: string;
6
+ quantity: number;
7
+ unitPrice: number | string;
8
+ subtotal: number | string;
9
+ }
10
+ export interface TlOrderTicketRef {
11
+ id: string;
12
+ name: string;
13
+ eventOccurrenceId: string | null;
14
+ }
15
+ export interface TlOrderData {
16
+ id: string;
17
+ orderNumber: string;
18
+ status: string;
19
+ paymentStatus?: string;
20
+ currency: string;
21
+ items: TlOrderItem[];
22
+ subtotal: number | string;
23
+ totalFees?: number | string;
24
+ totalTax?: number | string;
25
+ totalDiscounts?: number | string;
26
+ total: number | string;
27
+ tickets?: TlOrderTicketRef[];
28
+ createdAt?: string;
29
+ eventName?: string | null;
30
+ customerEmail?: string | null;
31
+ customer?: {
32
+ email?: string | null;
33
+ } | null;
34
+ }
35
+ /**
36
+ * One issued ticket, as the order view carries it: the redemption token a
37
+ * scanner reads, what it admits and whether it has been used.
38
+ */
39
+ export interface TlTicketData {
40
+ id: string;
41
+ barcode: string;
42
+ type: string;
43
+ status: string;
44
+ holderName: string | null;
45
+ name: string;
46
+ admissionStatus: string;
47
+ eventOccurrenceId: string | null;
48
+ seatLabel: string | null;
49
+ }
50
+ export interface TlResolvedOccurrence {
51
+ eventId: string;
52
+ eventName: string;
53
+ startsAt: string | null;
54
+ }
55
+ /** Money arrives as minor-unit integers or decimal strings ("50.00"). */
56
+ export declare function formatPrice(value: number | string | null | undefined, currency?: string): string;
57
+ export declare function formatDateTime(iso: string): string;
58
+ export declare function formatDate(iso: string): string;
59
+ /** Line items that are tickets (fees, taxes and discounts have their own type). */
60
+ export declare function ticketItems(order: TlOrderData): TlOrderItem[];
61
+ export declare function ticketCount(order: TlOrderData): number;
62
+ /** Event name as the order view exposes it: the first ticket line's name. */
63
+ export declare function orderEventName(order: TlOrderData): string | null;
64
+ export declare function orderCustomerEmail(order: TlOrderData | null, customer: unknown): string | null;
65
+ export declare function statusLabel(status: string | undefined): string;
66
+ export type StatusTone = 'success' | 'info' | 'warning' | 'error' | 'muted';
67
+ export declare function statusTone(status: string | undefined): StatusTone;
68
+ export declare function errorMessage(err: unknown, fallback: string): string;
69
+ /**
70
+ * Find the event and start time for an occurrence id. The order view only
71
+ * carries the occurrence id, so this walks the channel's listings (cached by
72
+ * live-api) until one owns the occurrence. Best effort: null when not found,
73
+ * and null on a client whose events manager cannot list or fetch, which is an
74
+ * older connector rather than a failure.
75
+ */
76
+ export declare function resolveOccurrence(client: ElementClient, occurrenceId: string, maxEvents?: number): Promise<TlResolvedOccurrence | null>;
77
+ /**
78
+ * The money rows the order views render.
79
+ *
80
+ * Two things about the sales order view drive this (backstage-api
81
+ * `src/domains/orders/services/order-line-items.service.ts`, `views/index.ts`):
82
+ *
83
+ * - the fee is a LINE ITEM, `type: 'fee'`, named after the channel fee
84
+ * profile's `serviceFeeLabel` (the API falls back to "Service Fee"). That
85
+ * name is the only place the label reaches a buyer-facing response.
86
+ * - `order.subtotal` INCLUDES the fee line, because the order's totals sum
87
+ * every non-discount, non-tax line into it; and `order.totalFees` is set at
88
+ * order creation and never rewritten by the recalculation, so it reads 0 on
89
+ * a real order that charged a fee. Rendering "Subtotal" straight off the
90
+ * order therefore hides the fee inside it, which is the bug this fixes.
91
+ *
92
+ * So the fee comes from the fee lines first and `totalFees` only as a
93
+ * fallback, the subtotal is the ticket lines alone, and the total is the
94
+ * order's own figure - never a sum of ours.
95
+ */
96
+ export interface TlOrderTotals {
97
+ currency: string;
98
+ /** Ticket lines only, in minor units: the fee is not folded in. */
99
+ subtotal: number;
100
+ fees: number;
101
+ feeLabel: string;
102
+ showFee: boolean;
103
+ tax: number;
104
+ discount: number;
105
+ /** What the buyer was charged, as the order reports it. */
106
+ total: number;
107
+ }
108
+ export declare function orderTotals(order: TlOrderData | null | undefined): TlOrderTotals;
109
+ /** The rows add up to the total the order reports. Asserted in the specs. */
110
+ export declare function totalsAgree(totals: TlOrderTotals): boolean;