@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,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.eventListCore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-event-list`: what the channel is selling, as a grid of cards.
|
|
6
|
+
*
|
|
7
|
+
* One read, and a re-read whenever the host changes what it is asking for. The
|
|
8
|
+
* cards themselves are `tl-event-card`, which has its own core; this one
|
|
9
|
+
* decides only what to ask for and what the grid says while there is nothing
|
|
10
|
+
* in it.
|
|
11
|
+
*
|
|
12
|
+
* Behaviour is what the Stencil component did, to the letter, including the
|
|
13
|
+
* two things that read like oversights and are kept:
|
|
14
|
+
*
|
|
15
|
+
* - there is NO error event. A failed listing is drawn and never announced,
|
|
16
|
+
* which makes this the only element of the eighteen that can fail silently
|
|
17
|
+
* as far as a host is concerned. Adding `tlError` would be a new event on a
|
|
18
|
+
* published element, so it is a decision for somebody rather than something
|
|
19
|
+
* to slip into a refactor.
|
|
20
|
+
* - the element paints nothing until the first listing lands. It holds
|
|
21
|
+
* skeleton markup and uses it for every read but the first, because the
|
|
22
|
+
* component awaited its read in `componentWillLoad` and Stencil does not
|
|
23
|
+
* paint until that resolves. The harness keeps that by waiting on the same
|
|
24
|
+
* thing; the core simply reports `loading` and lets the harness decide when
|
|
25
|
+
* a first paint happens, which is a rendering question.
|
|
26
|
+
*
|
|
27
|
+
* What did not come across is the `console.log` tracing, which a library
|
|
28
|
+
* should not do for its host (`tl-cart` dropped its own for the same reason).
|
|
29
|
+
*/
|
|
30
|
+
const define_js_1 = require("../define.js");
|
|
31
|
+
const STRINGS = {
|
|
32
|
+
errorTitle: 'Error loading events',
|
|
33
|
+
emptyTitle: 'No events available',
|
|
34
|
+
emptyMessage: 'Check back later for upcoming events.',
|
|
35
|
+
loadFailed: 'Failed to load events',
|
|
36
|
+
noListing: 'Events are not available',
|
|
37
|
+
};
|
|
38
|
+
/** The grid has a class per column count, and only four of them. */
|
|
39
|
+
const MIN_COLUMNS = 1;
|
|
40
|
+
const MAX_COLUMNS = 4;
|
|
41
|
+
exports.eventListCore = (0, define_js_1.defineElement)({
|
|
42
|
+
tag: 'tl-event-list',
|
|
43
|
+
docs: "What the channel is selling, as a grid of event cards, optionally narrowed to one category.",
|
|
44
|
+
category: 'discovery',
|
|
45
|
+
runtimes: ['web', 'native'],
|
|
46
|
+
tokens: [
|
|
47
|
+
'error',
|
|
48
|
+
'font-family',
|
|
49
|
+
'foreground',
|
|
50
|
+
'muted',
|
|
51
|
+
'muted-foreground',
|
|
52
|
+
'radius-lg',
|
|
53
|
+
'radius-sm',
|
|
54
|
+
'secondary',
|
|
55
|
+
'spacing-4',
|
|
56
|
+
'spacing-6',
|
|
57
|
+
'spacing-8',
|
|
58
|
+
],
|
|
59
|
+
sdkMethods: ['events.list'],
|
|
60
|
+
props: {
|
|
61
|
+
limit: { type: 'number', attr: 'limit', default: 12, docs: 'Maximum number of events to display' },
|
|
62
|
+
category: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
tsType: 'string | undefined',
|
|
65
|
+
attr: 'category',
|
|
66
|
+
docs: 'Category filter',
|
|
67
|
+
},
|
|
68
|
+
columns: { type: 'number', attr: 'columns', default: 3, docs: 'Columns for grid layout (1, 2, 3, or 4)' },
|
|
69
|
+
},
|
|
70
|
+
events: {
|
|
71
|
+
tlEventClick: { detail: '{ eventId: string; }', docs: 'Emitted when an event card is clicked' },
|
|
72
|
+
},
|
|
73
|
+
strings: STRINGS,
|
|
74
|
+
statuses: ['loading', 'error', 'empty', 'ready'],
|
|
75
|
+
state: () => ({ events: [], loading: true, error: null }),
|
|
76
|
+
mount: (ctx) => {
|
|
77
|
+
void ctx.actions.load();
|
|
78
|
+
},
|
|
79
|
+
status: ({ state }) => state.loading ? 'loading' : state.error ? 'error' : state.events.length === 0 ? 'empty' : 'ready',
|
|
80
|
+
derive: ({ state, props }) => ({
|
|
81
|
+
events: state.events,
|
|
82
|
+
columns: Math.min(Math.max(props.columns, MIN_COLUMNS), MAX_COLUMNS),
|
|
83
|
+
}),
|
|
84
|
+
actions: (ctx) => ({
|
|
85
|
+
/**
|
|
86
|
+
* Read the listing. Also what a harness calls when the limit or the
|
|
87
|
+
* category changes, because a different question deserves a fresh answer
|
|
88
|
+
* and the previous one is not narrowed down to it.
|
|
89
|
+
*/
|
|
90
|
+
async load() {
|
|
91
|
+
try {
|
|
92
|
+
ctx.setState({ loading: true, error: null });
|
|
93
|
+
const client = await ctx.whenClient();
|
|
94
|
+
const events = client.getEventsManager();
|
|
95
|
+
if (!events?.list)
|
|
96
|
+
throw new TypeError(ctx.strings.noListing);
|
|
97
|
+
const response = (await events.list({ limit: ctx.props.limit, category: ctx.props.category }));
|
|
98
|
+
ctx.setState({ events: (response?.items ?? []) });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// Shown and not announced: this element has no error event.
|
|
102
|
+
ctx.setState({ error: err instanceof Error ? err.message : ctx.strings.loadFailed });
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
ctx.setState({ loading: false });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
});
|
package/dist/cjs/cores/index.js
CHANGED
|
@@ -1,10 +1,73 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ELEMENT_CORES = exports.ticketSelectorCore = void 0;
|
|
3
|
+
exports.ELEMENT_CORES = exports.ticketSelectorCore = exports.promoCodeCore = exports.isAuthError = exports.POLL_DELAYS_MS = exports.orderTicketsCore = exports.orderConfirmationCore = exports.HORIZONTAL_MIN_WIDTH = exports.occurrenceSelectorCore = exports.myOrdersCore = exports.loginCore = exports.eventListCore = exports.toEventCardEvent = exports.eventCardCore = exports.checkoutWrapperCore = exports.OPEN_INTENT = exports.CLOSE_INTENT = exports.cartDrawerCore = exports.CART_COUNT_KEY = exports.cartBadgeCore = exports.cartCore = exports.buyTicketsWrapperCore = exports.buyTicketsButtonCore = void 0;
|
|
4
|
+
const buy_tickets_button_js_1 = require("./buy-tickets-button.js");
|
|
5
|
+
const buy_tickets_wrapper_js_1 = require("./buy-tickets-wrapper.js");
|
|
6
|
+
const cart_js_1 = require("./cart.js");
|
|
7
|
+
const cart_badge_js_1 = require("./cart-badge.js");
|
|
8
|
+
const cart_drawer_js_1 = require("./cart-drawer.js");
|
|
9
|
+
const checkout_wrapper_js_1 = require("./checkout-wrapper.js");
|
|
10
|
+
const event_card_js_1 = require("./event-card.js");
|
|
11
|
+
const event_list_js_1 = require("./event-list.js");
|
|
12
|
+
const login_js_1 = require("./login.js");
|
|
13
|
+
const my_orders_js_1 = require("./my-orders.js");
|
|
14
|
+
const occurrence_selector_js_1 = require("./occurrence-selector.js");
|
|
15
|
+
const order_confirmation_js_1 = require("./order-confirmation.js");
|
|
16
|
+
const order_tickets_js_1 = require("./order-tickets.js");
|
|
17
|
+
const promo_code_js_1 = require("./promo-code.js");
|
|
4
18
|
const ticket_selector_js_1 = require("./ticket-selector.js");
|
|
19
|
+
var buy_tickets_button_js_2 = require("./buy-tickets-button.js");
|
|
20
|
+
Object.defineProperty(exports, "buyTicketsButtonCore", { enumerable: true, get: function () { return buy_tickets_button_js_2.buyTicketsButtonCore; } });
|
|
21
|
+
var buy_tickets_wrapper_js_2 = require("./buy-tickets-wrapper.js");
|
|
22
|
+
Object.defineProperty(exports, "buyTicketsWrapperCore", { enumerable: true, get: function () { return buy_tickets_wrapper_js_2.buyTicketsWrapperCore; } });
|
|
23
|
+
var cart_js_2 = require("./cart.js");
|
|
24
|
+
Object.defineProperty(exports, "cartCore", { enumerable: true, get: function () { return cart_js_2.cartCore; } });
|
|
25
|
+
var cart_badge_js_2 = require("./cart-badge.js");
|
|
26
|
+
Object.defineProperty(exports, "cartBadgeCore", { enumerable: true, get: function () { return cart_badge_js_2.cartBadgeCore; } });
|
|
27
|
+
Object.defineProperty(exports, "CART_COUNT_KEY", { enumerable: true, get: function () { return cart_badge_js_2.CART_COUNT_KEY; } });
|
|
28
|
+
var cart_drawer_js_2 = require("./cart-drawer.js");
|
|
29
|
+
Object.defineProperty(exports, "cartDrawerCore", { enumerable: true, get: function () { return cart_drawer_js_2.cartDrawerCore; } });
|
|
30
|
+
Object.defineProperty(exports, "CLOSE_INTENT", { enumerable: true, get: function () { return cart_drawer_js_2.CLOSE_INTENT; } });
|
|
31
|
+
Object.defineProperty(exports, "OPEN_INTENT", { enumerable: true, get: function () { return cart_drawer_js_2.OPEN_INTENT; } });
|
|
32
|
+
var checkout_wrapper_js_2 = require("./checkout-wrapper.js");
|
|
33
|
+
Object.defineProperty(exports, "checkoutWrapperCore", { enumerable: true, get: function () { return checkout_wrapper_js_2.checkoutWrapperCore; } });
|
|
34
|
+
var event_card_js_2 = require("./event-card.js");
|
|
35
|
+
Object.defineProperty(exports, "eventCardCore", { enumerable: true, get: function () { return event_card_js_2.eventCardCore; } });
|
|
36
|
+
Object.defineProperty(exports, "toEventCardEvent", { enumerable: true, get: function () { return event_card_js_2.toEventCardEvent; } });
|
|
37
|
+
var event_list_js_2 = require("./event-list.js");
|
|
38
|
+
Object.defineProperty(exports, "eventListCore", { enumerable: true, get: function () { return event_list_js_2.eventListCore; } });
|
|
39
|
+
var login_js_2 = require("./login.js");
|
|
40
|
+
Object.defineProperty(exports, "loginCore", { enumerable: true, get: function () { return login_js_2.loginCore; } });
|
|
41
|
+
var my_orders_js_2 = require("./my-orders.js");
|
|
42
|
+
Object.defineProperty(exports, "myOrdersCore", { enumerable: true, get: function () { return my_orders_js_2.myOrdersCore; } });
|
|
43
|
+
var occurrence_selector_js_2 = require("./occurrence-selector.js");
|
|
44
|
+
Object.defineProperty(exports, "occurrenceSelectorCore", { enumerable: true, get: function () { return occurrence_selector_js_2.occurrenceSelectorCore; } });
|
|
45
|
+
Object.defineProperty(exports, "HORIZONTAL_MIN_WIDTH", { enumerable: true, get: function () { return occurrence_selector_js_2.HORIZONTAL_MIN_WIDTH; } });
|
|
46
|
+
var order_confirmation_js_2 = require("./order-confirmation.js");
|
|
47
|
+
Object.defineProperty(exports, "orderConfirmationCore", { enumerable: true, get: function () { return order_confirmation_js_2.orderConfirmationCore; } });
|
|
48
|
+
var order_tickets_js_2 = require("./order-tickets.js");
|
|
49
|
+
Object.defineProperty(exports, "orderTicketsCore", { enumerable: true, get: function () { return order_tickets_js_2.orderTicketsCore; } });
|
|
50
|
+
Object.defineProperty(exports, "POLL_DELAYS_MS", { enumerable: true, get: function () { return order_tickets_js_2.POLL_DELAYS_MS; } });
|
|
51
|
+
var promo_code_js_2 = require("./promo-code.js");
|
|
52
|
+
Object.defineProperty(exports, "isAuthError", { enumerable: true, get: function () { return promo_code_js_2.isAuthError; } });
|
|
53
|
+
Object.defineProperty(exports, "promoCodeCore", { enumerable: true, get: function () { return promo_code_js_2.promoCodeCore; } });
|
|
5
54
|
var ticket_selector_js_2 = require("./ticket-selector.js");
|
|
6
55
|
Object.defineProperty(exports, "ticketSelectorCore", { enumerable: true, get: function () { return ticket_selector_js_2.ticketSelectorCore; } });
|
|
7
56
|
/** Every core, by tag. */
|
|
8
57
|
exports.ELEMENT_CORES = {
|
|
58
|
+
[buy_tickets_button_js_1.buyTicketsButtonCore.tag]: buy_tickets_button_js_1.buyTicketsButtonCore,
|
|
59
|
+
[buy_tickets_wrapper_js_1.buyTicketsWrapperCore.tag]: buy_tickets_wrapper_js_1.buyTicketsWrapperCore,
|
|
60
|
+
[cart_js_1.cartCore.tag]: cart_js_1.cartCore,
|
|
61
|
+
[cart_badge_js_1.cartBadgeCore.tag]: cart_badge_js_1.cartBadgeCore,
|
|
62
|
+
[cart_drawer_js_1.cartDrawerCore.tag]: cart_drawer_js_1.cartDrawerCore,
|
|
63
|
+
[checkout_wrapper_js_1.checkoutWrapperCore.tag]: checkout_wrapper_js_1.checkoutWrapperCore,
|
|
64
|
+
[event_card_js_1.eventCardCore.tag]: event_card_js_1.eventCardCore,
|
|
65
|
+
[event_list_js_1.eventListCore.tag]: event_list_js_1.eventListCore,
|
|
66
|
+
[login_js_1.loginCore.tag]: login_js_1.loginCore,
|
|
67
|
+
[my_orders_js_1.myOrdersCore.tag]: my_orders_js_1.myOrdersCore,
|
|
68
|
+
[occurrence_selector_js_1.occurrenceSelectorCore.tag]: occurrence_selector_js_1.occurrenceSelectorCore,
|
|
69
|
+
[order_confirmation_js_1.orderConfirmationCore.tag]: order_confirmation_js_1.orderConfirmationCore,
|
|
70
|
+
[order_tickets_js_1.orderTicketsCore.tag]: order_tickets_js_1.orderTicketsCore,
|
|
71
|
+
[promo_code_js_1.promoCodeCore.tag]: promo_code_js_1.promoCodeCore,
|
|
9
72
|
[ticket_selector_js_1.ticketSelectorCore.tag]: ticket_selector_js_1.ticketSelectorCore,
|
|
10
73
|
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loginCore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-login`: magic-link sign-in. No password is handled anywhere in here.
|
|
6
|
+
*
|
|
7
|
+
* The customer enters an email, the client is asked to send a link, and the
|
|
8
|
+
* code that comes back is exchanged for a session either because the link
|
|
9
|
+
* landed on this page with `?code=` or because the customer typed the six
|
|
10
|
+
* digits out of the email. Both go through the same `exchange` action.
|
|
11
|
+
*
|
|
12
|
+
* The first core to read {@link ActionContext.query}, the seam this split
|
|
13
|
+
* added. An element that is the far end of an emailed link has to be able to
|
|
14
|
+
* see what the link said, and where that is written down is per runtime: the
|
|
15
|
+
* web has `window.location.search`, a native harness has its deep-link router.
|
|
16
|
+
* So the core says it reads `code`, once, on mount, and the harness says where
|
|
17
|
+
* from. A harness that supplies no query reads null and the element simply
|
|
18
|
+
* starts on the email step, which is what a page with no code in its URL does
|
|
19
|
+
* anyway.
|
|
20
|
+
*
|
|
21
|
+
* Behaviour is what the Stencil component did, to the letter, including:
|
|
22
|
+
*
|
|
23
|
+
* - the URL code is single use. It is taken once per instance and never read
|
|
24
|
+
* again, so a remount cannot spend a code that was already exchanged.
|
|
25
|
+
* - a failed exchange goes back to where it came from, and the two ways in
|
|
26
|
+
* are told apart: a typed code returns to the code step, a URL code returns
|
|
27
|
+
* to the email step.
|
|
28
|
+
* - a failed sign out is swallowed. The customer is shown as signed out and
|
|
29
|
+
* `tlAuthChanged` says so, because the session is gone locally either way,
|
|
30
|
+
* and that is what the component did.
|
|
31
|
+
*
|
|
32
|
+
* What did not come across is the `console.error` tracing, which a library
|
|
33
|
+
* should not do for its host (`tl-cart` dropped its own for the same reason).
|
|
34
|
+
*/
|
|
35
|
+
const define_js_1 = require("../define.js");
|
|
36
|
+
const orders_js_1 = require("../orders.js");
|
|
37
|
+
const STRINGS = {
|
|
38
|
+
signedInPrefix: 'Signed in as ',
|
|
39
|
+
fallbackAccount: 'your account',
|
|
40
|
+
signOut: 'Sign out',
|
|
41
|
+
verifyingTitle: 'Signing you in…',
|
|
42
|
+
verifyingLead: 'Just a moment.',
|
|
43
|
+
sentTitle: 'Check your inbox',
|
|
44
|
+
// The lead reads "We've sent a sign-in link to <strong>you@…</strong>. Open
|
|
45
|
+
// it on this device to continue." Split in two because the address is
|
|
46
|
+
// emphasised in the middle of it, and the shape has no way to say "this copy
|
|
47
|
+
// has a slot in it" - see the README's note on what a core still cannot
|
|
48
|
+
// express.
|
|
49
|
+
sentLeadBefore: "We've sent a sign-in link to ",
|
|
50
|
+
sentLeadAfter: '. Open it on this device to continue.',
|
|
51
|
+
sentAgain: 'Sent again. Check your inbox.',
|
|
52
|
+
resendLink: 'Resend link',
|
|
53
|
+
resending: 'Sending…',
|
|
54
|
+
enterCodeInstead: 'Enter the code instead',
|
|
55
|
+
differentEmail: 'Use a different email',
|
|
56
|
+
codeTitle: 'Enter your code',
|
|
57
|
+
codeLeadBefore: 'Type the 6-digit code from the email we sent to ',
|
|
58
|
+
codeLeadAfter: '.',
|
|
59
|
+
codeLabel: 'Code',
|
|
60
|
+
codePlaceholder: '123456',
|
|
61
|
+
codeSubmit: 'Sign in',
|
|
62
|
+
checking: 'Checking…',
|
|
63
|
+
back: 'Back',
|
|
64
|
+
emailLead: "Enter your email and we'll send you a secure sign-in link. No password needed.",
|
|
65
|
+
emailLabel: 'Email address',
|
|
66
|
+
emailPlaceholder: 'you@example.com',
|
|
67
|
+
emailSubmit: 'Email me a sign-in link',
|
|
68
|
+
sendingLink: 'Sending link…',
|
|
69
|
+
enterEmail: 'Enter your email address',
|
|
70
|
+
enterCode: 'Enter the code from your email',
|
|
71
|
+
codeRejected: 'That code did not work. Check it and try again, or request a new link.',
|
|
72
|
+
requestFailed: 'Could not send your sign-in link',
|
|
73
|
+
unavailable: 'Sign-in is not available right now',
|
|
74
|
+
unsupported: 'Sign-in is not available with this SDK',
|
|
75
|
+
};
|
|
76
|
+
exports.loginCore = (0, define_js_1.defineElement)({
|
|
77
|
+
tag: 'tl-login',
|
|
78
|
+
docs: 'Magic-link sign-in: an email in, a link or a six-digit code back, and a session. No password is ever handled.',
|
|
79
|
+
category: 'account',
|
|
80
|
+
runtimes: ['web', 'native'],
|
|
81
|
+
tokens: [
|
|
82
|
+
'background',
|
|
83
|
+
'border',
|
|
84
|
+
'error',
|
|
85
|
+
'font-family',
|
|
86
|
+
'font-family-mono',
|
|
87
|
+
'foreground',
|
|
88
|
+
'muted-foreground',
|
|
89
|
+
'primary',
|
|
90
|
+
'primary-foreground',
|
|
91
|
+
'radius-lg',
|
|
92
|
+
'radius-md',
|
|
93
|
+
'secondary',
|
|
94
|
+
'spacing-3',
|
|
95
|
+
'spacing-4',
|
|
96
|
+
'spacing-6',
|
|
97
|
+
'success',
|
|
98
|
+
],
|
|
99
|
+
sdkMethods: ['auth.requestMagicLink', 'auth.exchange', 'auth.logout', 'on'],
|
|
100
|
+
props: {
|
|
101
|
+
heading: { type: 'string', attr: 'heading', default: 'Sign in', docs: 'Heading for the email step.' },
|
|
102
|
+
allowCodeEntry: {
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
attr: 'allow-code-entry',
|
|
105
|
+
default: true,
|
|
106
|
+
docs: 'Offer a "enter the code" path alongside the emailed link (default true).',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
events: {
|
|
110
|
+
tlAuthChanged: { detail: 'TlAuthChanged', docs: 'signed in or out ({ customer, authenticated })' },
|
|
111
|
+
tlMagicLinkSent: { detail: '{ email: string; }', docs: 'a link was requested for an email' },
|
|
112
|
+
tlError: { detail: '{ message: string; }', docs: 'request or code exchange failed' },
|
|
113
|
+
},
|
|
114
|
+
strings: STRINGS,
|
|
115
|
+
statuses: ['email', 'sent', 'code', 'verifying', 'signed-in'],
|
|
116
|
+
state: () => ({
|
|
117
|
+
phase: 'email',
|
|
118
|
+
email: '',
|
|
119
|
+
code: '',
|
|
120
|
+
busy: false,
|
|
121
|
+
error: null,
|
|
122
|
+
notice: null,
|
|
123
|
+
customer: null,
|
|
124
|
+
authAvailable: false,
|
|
125
|
+
urlCodeUsed: false,
|
|
126
|
+
}),
|
|
127
|
+
/**
|
|
128
|
+
* On screen: wait for the client, read who is signed in, follow both auth
|
|
129
|
+
* signals, then spend the code the link arrived with, if it did.
|
|
130
|
+
*
|
|
131
|
+
* The code is taken from the query whether or not it gets used, which is
|
|
132
|
+
* what makes it single use: arriving already signed in burns it, exactly as
|
|
133
|
+
* the component did.
|
|
134
|
+
*/
|
|
135
|
+
mount: (ctx) => {
|
|
136
|
+
void (async () => {
|
|
137
|
+
let client;
|
|
138
|
+
try {
|
|
139
|
+
client = await ctx.whenClient();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
if (!ctx.active())
|
|
143
|
+
return;
|
|
144
|
+
ctx.actions.connectFailed();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!ctx.active())
|
|
148
|
+
return;
|
|
149
|
+
const auth = client.getAuthManager?.();
|
|
150
|
+
if (!auth) {
|
|
151
|
+
ctx.actions.unsupported();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
ctx.setState({ authAvailable: true });
|
|
155
|
+
ctx.actions.syncFromClient();
|
|
156
|
+
// The session hydrates after the client publishes; follow both signals.
|
|
157
|
+
for (const event of ['auth:changed', 'session:changed']) {
|
|
158
|
+
const off = client.on?.(event, () => ctx.actions.syncFromClient());
|
|
159
|
+
if (typeof off === 'function')
|
|
160
|
+
ctx.onTeardown(off);
|
|
161
|
+
}
|
|
162
|
+
const urlCode = ctx.state.urlCodeUsed ? null : ctx.query.get('code');
|
|
163
|
+
if (urlCode) {
|
|
164
|
+
ctx.setState({ urlCodeUsed: true });
|
|
165
|
+
if (!auth.authenticated)
|
|
166
|
+
await ctx.actions.exchange(urlCode);
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
},
|
|
170
|
+
status: ({ state }) => state.phase,
|
|
171
|
+
derive: ({ state, strings, props }) => ({
|
|
172
|
+
signedInEmail: state.customer?.email ?? strings.fallbackAccount,
|
|
173
|
+
inputsDisabled: state.busy || !state.authAvailable,
|
|
174
|
+
emailSubmitLabel: state.busy ? strings.sendingLink : strings.emailSubmit,
|
|
175
|
+
resendLabel: state.busy ? strings.resending : strings.resendLink,
|
|
176
|
+
codeSubmitLabel: state.busy ? strings.checking : strings.codeSubmit,
|
|
177
|
+
showCodeEntry: props.allowCodeEntry,
|
|
178
|
+
}),
|
|
179
|
+
// Named so `submitCode` can call `exchange`: the action context does not
|
|
180
|
+
// carry the actions, only a mount's does.
|
|
181
|
+
actions: (ctx) => {
|
|
182
|
+
/** The auth manager as it stands, or null on a client that has none. */
|
|
183
|
+
const authManager = () => ctx.optionalClient()?.getAuthManager?.() ?? null;
|
|
184
|
+
const self = {
|
|
185
|
+
/** Typing clears whatever the last attempt complained about. */
|
|
186
|
+
setEmail(value) {
|
|
187
|
+
ctx.setState({ email: value, error: null });
|
|
188
|
+
},
|
|
189
|
+
setCode(value) {
|
|
190
|
+
ctx.setState({ code: value, error: null });
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Ask for a link. The response always reports success, so nothing here
|
|
194
|
+
* can reveal whether an account exists.
|
|
195
|
+
*/
|
|
196
|
+
async requestLink(resend = false) {
|
|
197
|
+
const email = ctx.state.email.trim();
|
|
198
|
+
if (!email) {
|
|
199
|
+
ctx.setState({ error: ctx.strings.enterEmail });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const auth = authManager();
|
|
203
|
+
if (!auth)
|
|
204
|
+
return;
|
|
205
|
+
ctx.setState({ busy: true, error: null, notice: null });
|
|
206
|
+
try {
|
|
207
|
+
await auth.requestMagicLink({ email });
|
|
208
|
+
ctx.setState({ phase: 'sent', notice: resend ? ctx.strings.sentAgain : null });
|
|
209
|
+
ctx.emit('tlMagicLinkSent', { email });
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
const message = (0, orders_js_1.errorMessage)(err, ctx.strings.requestFailed);
|
|
213
|
+
ctx.setState({ error: message });
|
|
214
|
+
ctx.emit('tlError', { message });
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
ctx.setState({ busy: false });
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
/** The code the customer typed. */
|
|
221
|
+
async submitCode() {
|
|
222
|
+
await self.exchange(ctx.state.code.trim());
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Trade a code for a session. On failure the step it came from decides
|
|
226
|
+
* where it goes back to: a typed code returns to the code step, a URL
|
|
227
|
+
* code (which arrives while the phase is still `email`) returns to the
|
|
228
|
+
* email step.
|
|
229
|
+
*/
|
|
230
|
+
async exchange(code) {
|
|
231
|
+
if (!code) {
|
|
232
|
+
ctx.setState({ error: ctx.strings.enterCode });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const auth = authManager();
|
|
236
|
+
if (!auth)
|
|
237
|
+
return;
|
|
238
|
+
const previous = ctx.state.phase;
|
|
239
|
+
ctx.setState({ busy: true, error: null, phase: 'verifying' });
|
|
240
|
+
try {
|
|
241
|
+
const res = (await auth.exchange(code));
|
|
242
|
+
const customer = auth.customer ?? res?.customer ?? null;
|
|
243
|
+
ctx.setState({ customer, phase: 'signed-in', code: '' });
|
|
244
|
+
ctx.emit('tlAuthChanged', { customer, authenticated: true });
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
ctx.setState({
|
|
248
|
+
phase: previous === 'verifying' || previous === 'email' ? 'email' : 'code',
|
|
249
|
+
error: ctx.strings.codeRejected,
|
|
250
|
+
});
|
|
251
|
+
ctx.emit('tlError', { message: ctx.strings.codeRejected });
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
ctx.setState({ busy: false });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
/**
|
|
258
|
+
* Sign out. A failure is swallowed on purpose: the session is gone from
|
|
259
|
+
* this page either way, so the customer is shown as signed out and the
|
|
260
|
+
* host is told so.
|
|
261
|
+
*/
|
|
262
|
+
async signOut() {
|
|
263
|
+
const auth = authManager();
|
|
264
|
+
if (!auth)
|
|
265
|
+
return;
|
|
266
|
+
ctx.setState({ busy: true });
|
|
267
|
+
try {
|
|
268
|
+
await auth.logout();
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Shown as signed out regardless, which is what the component did.
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
ctx.setState({ busy: false, customer: null, phase: 'email' });
|
|
275
|
+
ctx.emit('tlAuthChanged', { customer: null, authenticated: false });
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
/** The steps a buyer can walk between by hand. */
|
|
279
|
+
goTo(phase) {
|
|
280
|
+
ctx.setState({ phase });
|
|
281
|
+
},
|
|
282
|
+
/**
|
|
283
|
+
* The client says the session changed. Signed in wins outright; signed
|
|
284
|
+
* out only pulls the element back when it was showing the signed-in
|
|
285
|
+
* step, so a customer half way through typing a code is left alone.
|
|
286
|
+
*/
|
|
287
|
+
syncFromClient() {
|
|
288
|
+
const auth = authManager();
|
|
289
|
+
if (!auth)
|
|
290
|
+
return;
|
|
291
|
+
if (auth.authenticated) {
|
|
292
|
+
ctx.setState({ customer: auth.customer ?? null, phase: 'signed-in' });
|
|
293
|
+
}
|
|
294
|
+
else if (ctx.state.phase === 'signed-in') {
|
|
295
|
+
ctx.setState({ customer: null, phase: 'email' });
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
/** The host never started Live. Said on screen, and nowhere else. */
|
|
299
|
+
connectFailed() {
|
|
300
|
+
ctx.setState({ error: ctx.strings.unavailable });
|
|
301
|
+
},
|
|
302
|
+
/** A connector with no auth accessor cannot sign anybody in. */
|
|
303
|
+
unsupported() {
|
|
304
|
+
ctx.setState({ error: ctx.strings.unsupported });
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
return self;
|
|
308
|
+
},
|
|
309
|
+
});
|