@ticketlayer/elements-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -4
- package/dist/cjs/cores/buy-tickets-button.js +213 -0
- package/dist/cjs/cores/buy-tickets-wrapper.js +106 -0
- package/dist/cjs/cores/cart-badge.js +150 -0
- package/dist/cjs/cores/cart-drawer.js +122 -0
- package/dist/cjs/cores/cart.js +247 -0
- package/dist/cjs/cores/checkout-wrapper.js +75 -0
- package/dist/cjs/cores/event-card.js +209 -0
- package/dist/cjs/cores/event-list.js +109 -0
- package/dist/cjs/cores/index.js +64 -1
- package/dist/cjs/cores/login.js +309 -0
- package/dist/cjs/cores/my-orders.js +221 -0
- package/dist/cjs/cores/occurrence-selector.js +528 -0
- package/dist/cjs/cores/order-confirmation.js +301 -0
- package/dist/cjs/cores/order-tickets.js +172 -0
- package/dist/cjs/cores/promo-code.js +136 -0
- package/dist/cjs/cores/ticket-selector.js +30 -0
- package/dist/cjs/define.js +76 -0
- package/dist/cjs/index.js +17 -1
- package/dist/cjs/manifest.js +5 -0
- package/dist/cjs/orders.js +169 -0
- package/dist/esm/client.d.ts +106 -0
- package/dist/esm/cores/buy-tickets-button.d.ts +55 -0
- package/dist/esm/cores/buy-tickets-button.js +210 -0
- package/dist/esm/cores/buy-tickets-wrapper.d.ts +27 -0
- package/dist/esm/cores/buy-tickets-wrapper.js +103 -0
- package/dist/esm/cores/cart-badge.d.ts +48 -0
- package/dist/esm/cores/cart-badge.js +147 -0
- package/dist/esm/cores/cart-drawer.d.ts +44 -0
- package/dist/esm/cores/cart-drawer.js +119 -0
- package/dist/esm/cores/cart.d.ts +105 -0
- package/dist/esm/cores/cart.js +244 -0
- package/dist/esm/cores/checkout-wrapper.d.ts +22 -0
- package/dist/esm/cores/checkout-wrapper.js +72 -0
- package/dist/esm/cores/event-card.d.ts +80 -0
- package/dist/esm/cores/event-card.js +205 -0
- package/dist/esm/cores/event-list.d.ts +35 -0
- package/dist/esm/cores/event-list.js +106 -0
- package/dist/esm/cores/index.d.ts +28 -0
- package/dist/esm/cores/index.js +42 -0
- package/dist/esm/cores/login.d.ts +125 -0
- package/dist/esm/cores/login.js +306 -0
- package/dist/esm/cores/my-orders.d.ts +63 -0
- package/dist/esm/cores/my-orders.js +218 -0
- package/dist/esm/cores/occurrence-selector.d.ts +170 -0
- package/dist/esm/cores/occurrence-selector.js +525 -0
- package/dist/esm/cores/order-confirmation.d.ts +99 -0
- package/dist/esm/cores/order-confirmation.js +298 -0
- package/dist/esm/cores/order-tickets.d.ts +46 -0
- package/dist/esm/cores/order-tickets.js +169 -0
- package/dist/esm/cores/promo-code.d.ts +56 -0
- package/dist/esm/cores/promo-code.js +132 -0
- package/dist/esm/cores/ticket-selector.js +30 -0
- package/dist/esm/define.d.ts +146 -0
- package/dist/esm/define.js +75 -0
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.js +2 -1
- package/dist/esm/manifest.d.ts +12 -0
- package/dist/esm/manifest.js +5 -0
- package/dist/esm/orders.d.ts +110 -0
- package/dist/esm/orders.js +154 -0
- package/package.json +2 -2
|
@@ -0,0 +1,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;
|
package/dist/esm/define.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/esm/define.js
CHANGED
|
@@ -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);
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/esm/manifest.d.ts
CHANGED
|
@@ -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
|
package/dist/esm/manifest.js
CHANGED
|
@@ -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;
|