@semics-tech/emdash-events-suite 1.0.0-rc.3 → 1.0.0-rc.5
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/dist/ts/index.d.ts +4 -1
- package/dist/ts/index.js +9 -3
- package/package.json +3 -3
- package/src/astro/BookingWithWaiver.astro +4 -1
- package/src/astro/WaiverPage.astro +28 -11
- package/src/astro/lib/flow.ts +18 -2
- package/src/astro/lib/form.ts +24 -0
- package/src/astro/lib/keep-form.ts +77 -0
- package/src/astro/virtual.d.ts +2 -0
package/dist/ts/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PageOptions } from "@semics-tech/emdash-events/integration";
|
|
1
2
|
import type { PluginDescriptor } from "emdash";
|
|
2
3
|
/**
|
|
3
4
|
* A core the suite links: a sandboxed descriptor to load from npm (the product package's default
|
|
@@ -12,6 +13,8 @@ export interface EventsSuiteOptions {
|
|
|
12
13
|
events?: SuiteCore;
|
|
13
14
|
/** Default: the Free E-sign core of `@semics-tech/emdash-esign`. */
|
|
14
15
|
esign?: SuiteCore;
|
|
16
|
+
/** How `/booking` and `/_events/waiver` look on this site: theme, stylesheet, brand, event page path. */
|
|
17
|
+
page?: PageOptions;
|
|
15
18
|
}
|
|
16
19
|
/** The slice of Astro's integration API the injected pages need (no `astro` dependency). */
|
|
17
20
|
interface SetupArgs {
|
|
@@ -58,7 +61,7 @@ export declare const PAIR_ROUTE = "/_events/pair";
|
|
|
58
61
|
export declare function suiteIntegration(ids: {
|
|
59
62
|
eventsPluginId: string;
|
|
60
63
|
esignPluginId: string;
|
|
61
|
-
}): SuiteAstroIntegration;
|
|
64
|
+
}, page?: PageOptions): SuiteAstroIntegration;
|
|
62
65
|
/**
|
|
63
66
|
* Events and E-sign on one site (SX-FR-01). Every part is told the id of each core it calls, taken
|
|
64
67
|
* from the descriptor or the registry id given here, never assumed:
|
package/dist/ts/index.js
CHANGED
|
@@ -28,10 +28,11 @@ const RECONCILE_MODULE = "@semics-tech/emdash-events-suite/astro/lib/reconcile";
|
|
|
28
28
|
* Injects `/_events/waiver` (the booking and signing steps of `<BookingWithWaiver>`) and gives it,
|
|
29
29
|
* through a virtual module, the id of each core. `eventsSuite()` adds it for you.
|
|
30
30
|
*/
|
|
31
|
-
export function suiteIntegration(ids) {
|
|
31
|
+
export function suiteIntegration(ids, page = {}) {
|
|
32
32
|
const config = [
|
|
33
33
|
`export const eventsPluginId = ${JSON.stringify(ids.eventsPluginId)};`,
|
|
34
34
|
`export const esignPluginId = ${JSON.stringify(ids.esignPluginId)};`,
|
|
35
|
+
`export const page = ${JSON.stringify(page)};`,
|
|
35
36
|
].join("\n");
|
|
36
37
|
return {
|
|
37
38
|
name: "semics-events-suite",
|
|
@@ -85,9 +86,14 @@ export function eventsSuite(options = {}) {
|
|
|
85
86
|
sandboxed: [events.descriptor, esign.descriptor].filter((d) => d !== undefined),
|
|
86
87
|
plugins: [eventsUi(), esignUi()],
|
|
87
88
|
integrations: [
|
|
88
|
-
eventsIntegration({
|
|
89
|
+
eventsIntegration({
|
|
90
|
+
pluginId: events.id,
|
|
91
|
+
esignPluginId: esign.id,
|
|
92
|
+
reconcile: RECONCILE_MODULE,
|
|
93
|
+
...(options.page ? { page: options.page } : {}),
|
|
94
|
+
}),
|
|
89
95
|
esignIntegration({ pluginId: esign.id }),
|
|
90
|
-
suiteIntegration({ eventsPluginId: events.id, esignPluginId: esign.id }),
|
|
96
|
+
suiteIntegration({ eventsPluginId: events.id, esignPluginId: esign.id }, options.page),
|
|
91
97
|
],
|
|
92
98
|
pluginIds: { events: events.id, esign: esign.id },
|
|
93
99
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semics-tech/emdash-events-suite",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Links semics Events and E-sign on one EmDash site: one config call and a booking flow with inline waiver signing",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"src/astro"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@semics-tech/emdash-
|
|
26
|
-
"@semics-tech/emdash-
|
|
25
|
+
"@semics-tech/emdash-esign": "1.0.0-rc.5",
|
|
26
|
+
"@semics-tech/emdash-events": "1.0.0-rc.5"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"emdash": ">=1.2.0 <2"
|
|
@@ -94,12 +94,13 @@ const single = ticketTypes.length === 1 ? ticketTypes[0] : undefined;
|
|
|
94
94
|
!onSale ? (
|
|
95
95
|
<p data-testid="booking-unavailable">Booking is not available for this event right now.</p>
|
|
96
96
|
) : (
|
|
97
|
-
<form method="post" action={WAIVER_PATH}>
|
|
97
|
+
<form method="post" action={WAIVER_PATH} data-keep-values>
|
|
98
98
|
<p class="semics-form-alert" role="alert" data-testid="waiver-alert" hidden={!alert}>{alert}</p>
|
|
99
99
|
<input type="hidden" name="intent" value="book" />
|
|
100
100
|
<input type="hidden" name="eventId" value={eventId} />
|
|
101
101
|
<input type="hidden" name="returnTo" value={returnTo} />
|
|
102
102
|
<input type="hidden" name="idempotencyKey" value={newIdempotencyKey()} />
|
|
103
|
+
{data?.waiverEach && <input type="hidden" name="waiverEach" value="1" />}
|
|
103
104
|
|
|
104
105
|
{picker && (
|
|
105
106
|
<div class="semics-field semics-picker-field">
|
|
@@ -301,6 +302,8 @@ const single = ticketTypes.length === 1 ? ticketTypes[0] : undefined;
|
|
|
301
302
|
</section>
|
|
302
303
|
|
|
303
304
|
<script>
|
|
305
|
+
// In this order: the kept values go back first, then the picker shows the date the select holds.
|
|
306
|
+
import "./lib/keep-form";
|
|
304
307
|
import "@semics-tech/emdash-events/client/picker";
|
|
305
308
|
</script>
|
|
306
309
|
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
// Injected at `/_events/waiver` by the suite (SX-FR-02, SX-FR-03): the booking post of
|
|
3
3
|
// <BookingWithWaiver> and the inline waiver ceremony, one signer after another, on one page.
|
|
4
4
|
// Signing tokens live only in an HttpOnly cookie scoped to this path and in POST bodies to E-sign;
|
|
5
|
-
// receipts are forwarded to Events in-process. The page renders E-sign's <SigningCeremony> and
|
|
6
|
-
//
|
|
7
|
-
// pad and stylesheet), `Referrer-Policy: no-referrer` and `no-store`.
|
|
8
|
-
import { esignPluginId, eventsPluginId } from "virtual:semics-events-suite/config";
|
|
5
|
+
// receipts are forwarded to Events in-process. The page renders E-sign's <SigningCeremony> and, inline,
|
|
6
|
+
// only Events' page stylesheet, so it sends E-sign's `ceremonyCsp()` (hashes of the ceremony's own script,
|
|
7
|
+
// pad and stylesheet, and of the page's), `Referrer-Policy: no-referrer` and `no-store`.
|
|
8
|
+
import { esignPluginId, eventsPluginId, page } from "virtual:semics-events-suite/config";
|
|
9
9
|
import SigningCeremony from "@semics-tech/emdash-esign/astro/SigningCeremony.astro";
|
|
10
10
|
import { ceremonyCsp } from "@semics-tech/emdash-esign/astro/lib/csp";
|
|
11
|
+
import PageBrand from "@semics-tech/emdash-events/astro/PageBrand.astro";
|
|
11
12
|
import { crossSiteResponse } from "@semics-tech/emdash-events/astro/lib/checkout-form";
|
|
13
|
+
import { pageCss } from "@semics-tech/emdash-events/astro/lib/page";
|
|
12
14
|
|
|
13
|
-
import { handleWaiverRequest } from "./lib/flow";
|
|
15
|
+
import { handleWaiverRequest, leavesSite } from "./lib/flow";
|
|
14
16
|
import { WAIVER_MESSAGES, WAIVER_PATH } from "./lib/form";
|
|
15
17
|
import { waiverSlot } from "./lib/slot";
|
|
16
18
|
|
|
@@ -18,16 +20,20 @@ export const prerender = false;
|
|
|
18
20
|
|
|
19
21
|
const outcome = await handleWaiverRequest(
|
|
20
22
|
{ locals: Astro.locals, url: Astro.url, request: Astro.request, cookie: (name) => Astro.cookies.get(name)?.value },
|
|
21
|
-
{ eventsPluginId, esignPluginId },
|
|
23
|
+
{ eventsPluginId, esignPluginId, ...(page.eventPath ? { eventPath: page.eventPath } : {}) },
|
|
22
24
|
);
|
|
23
25
|
if (outcome.type === "forbidden") return crossSiteResponse();
|
|
24
26
|
const cookies = outcome.type === "redirect" || outcome.type === "render" ? outcome.cookies : [];
|
|
25
27
|
for (const cookie of cookies) Astro.cookies.set(cookie.name, cookie.value, cookie.options);
|
|
26
|
-
if (outcome.type === "redirect") return Astro.redirect(outcome.location, 303);
|
|
28
|
+
if (outcome.type === "redirect" && !leavesSite(outcome.location, Astro.url)) return Astro.redirect(outcome.location, 303);
|
|
29
|
+
// The payment page is another site, which the CSP below stops a post redirecting to (T155): a page goes there instead.
|
|
30
|
+
const payment = outcome.type === "redirect" ? outcome.location : undefined;
|
|
27
31
|
|
|
28
32
|
Astro.response.headers.set("Referrer-Policy", "no-referrer");
|
|
29
33
|
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
30
|
-
|
|
34
|
+
const css = pageCss(page);
|
|
35
|
+
const stylesheet = page.stylesheet ? new URL(page.stylesheet, Astro.url).href : undefined;
|
|
36
|
+
Astro.response.headers.set("Content-Security-Policy", await ceremonyCsp({ styles: [css], stylesheet }));
|
|
31
37
|
|
|
32
38
|
const render = outcome.type === "render" ? outcome : undefined;
|
|
33
39
|
const ref = outcome.type === "render" || outcome.type === "error" ? (outcome.ref ?? "") : "";
|
|
@@ -54,10 +60,20 @@ const signer = render?.view.kind === "ready" ? render.view.session.signer : unde
|
|
|
54
60
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
55
61
|
<meta name="robots" content="noindex" />
|
|
56
62
|
<meta name="referrer" content="no-referrer" />
|
|
57
|
-
<title>Sign the waiver</title>
|
|
63
|
+
<title>{payment ? "Continue to payment" : "Sign the waiver"}</title>
|
|
64
|
+
{payment && <meta http-equiv="refresh" content={`0;url=${payment}`} data-testid="waiver-pay-refresh" />}
|
|
65
|
+
<style is:inline set:html={css}></style>
|
|
66
|
+
{page.stylesheet && <link rel="stylesheet" href={page.stylesheet} />}
|
|
58
67
|
</head>
|
|
59
|
-
<body>
|
|
60
|
-
<
|
|
68
|
+
<body class="semics-page">
|
|
69
|
+
<PageBrand brand={page.brand} />
|
|
70
|
+
{payment ? (
|
|
71
|
+
<main class="semics-esign semics-sign semics-page-main" data-testid="waiver-page" data-step="pay">
|
|
72
|
+
<h1>Continue to payment</h1>
|
|
73
|
+
<p><a href={payment} data-testid="waiver-pay-link">Go to the payment page</a></p>
|
|
74
|
+
</main>
|
|
75
|
+
) : (
|
|
76
|
+
<main class="semics-esign semics-sign semics-page-main" data-testid="waiver-page" data-step={render ? "sign" : "error"}>
|
|
61
77
|
<h1>Sign the waiver</h1>
|
|
62
78
|
{error && <p role="alert" data-testid="waiver-error">{error}</p>}
|
|
63
79
|
{expired && eventLink && <p><a href={eventLink} data-testid="waiver-book-again">Book again</a></p>}
|
|
@@ -89,5 +105,6 @@ const signer = render?.view.kind === "ready" ? render.view.session.signer : unde
|
|
|
89
105
|
)}
|
|
90
106
|
{ref && <p><a href={`/booking?ref=${ref}`}>Back to your booking</a></p>}
|
|
91
107
|
</main>
|
|
108
|
+
)}
|
|
92
109
|
</body>
|
|
93
110
|
</html>
|
package/src/astro/lib/flow.ts
CHANGED
|
@@ -26,12 +26,13 @@ import {
|
|
|
26
26
|
bookingCookieOptions,
|
|
27
27
|
isCrossSiteRequest,
|
|
28
28
|
} from "@semics-tech/emdash-events/astro/lib/checkout-form";
|
|
29
|
-
import {
|
|
29
|
+
import { eventPageHref } from "@semics-tech/emdash-events/astro/lib/page";
|
|
30
30
|
import { isPayUrl } from "@semics-tech/emdash-events/astro/lib/payment";
|
|
31
31
|
import { forwardReceipt } from "@semics-tech/emdash-events/astro/lib/receipt";
|
|
32
32
|
import { newIdempotencyKey } from "@semics-tech/emdash-events/astro/lib/widget";
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
|
+
checkWaiverSeats,
|
|
35
36
|
readWaiverForm,
|
|
36
37
|
type SeatPart,
|
|
37
38
|
safeReturnPath,
|
|
@@ -48,6 +49,8 @@ export const FLOW_STEPS = ["book", "sign", "pay", "done"] as const;
|
|
|
48
49
|
export interface SuiteIds {
|
|
49
50
|
eventsPluginId: string;
|
|
50
51
|
esignPluginId: string;
|
|
52
|
+
/** The site's event page for "Book again" (`page.eventPath`); default `/events/{slug}`. */
|
|
53
|
+
eventPath?: string;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export interface WaiverRequestContext {
|
|
@@ -100,6 +103,14 @@ export interface BookingAnswer {
|
|
|
100
103
|
waiver?: { refs: string[]; voids?: Record<string, string> };
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Whether a redirect leaves this site (T155). A form on the signing page cannot be redirected there:
|
|
108
|
+
* its CSP's `form-action 'self'` also covers where the post redirects, so the browser stops on the page.
|
|
109
|
+
*/
|
|
110
|
+
export function leavesSite(location: string, site: URL): boolean {
|
|
111
|
+
return new URL(location, site).origin !== site.origin;
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
/** Booking statuses with nothing left to sign for: the places are gone (T151). */
|
|
104
115
|
const ENDED = ["expired", "cancelled", "declined", "refunded", "refunded_unavailable"];
|
|
105
116
|
|
|
@@ -254,6 +265,11 @@ async function book(ctx: WaiverRequestContext, form: FormData, ids: SuiteIds, de
|
|
|
254
265
|
const read = readWaiverForm(form);
|
|
255
266
|
if (!read.ok) return refuse(read.code, read.fields);
|
|
256
267
|
const order = read.order;
|
|
268
|
+
// An order the waiver rules refuse never reserves: no booking is left behind, and no "cancelled" email sent.
|
|
269
|
+
if (form.get("waiverEach") === "1") {
|
|
270
|
+
const seats = checkWaiverSeats(order);
|
|
271
|
+
if (seats) return refuse(seats.code, seats.fields);
|
|
272
|
+
}
|
|
257
273
|
|
|
258
274
|
const reserved = await post<{ bookingRef?: string; accessToken?: string }>(
|
|
259
275
|
call,
|
|
@@ -439,7 +455,7 @@ async function sign(
|
|
|
439
455
|
esignPluginId: ids.esignPluginId,
|
|
440
456
|
});
|
|
441
457
|
}
|
|
442
|
-
const href = booking.event?.slug
|
|
458
|
+
const href = eventPageHref(booking.event?.slug, ids.eventPath);
|
|
443
459
|
return { type: "error", ref, code: "BOOKING_EXPIRED", ...(href ? { href } : {}) };
|
|
444
460
|
}
|
|
445
461
|
const withBooking = booking ? { booking } : {};
|
package/src/astro/lib/form.ts
CHANGED
|
@@ -120,6 +120,30 @@ export function readWaiverForm(form: FormData): WaiverFormResult {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Adults one order can have invited by email (Events Pro's `MAX_EMAIL_SIGNERS`). */
|
|
124
|
+
const MAX_EMAIL_SIGNERS = 10;
|
|
125
|
+
const norm = (value: string) => value.trim().toLowerCase();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Events Pro's waiver rules for an event signed per attendee (`planWaiverIntents`), asked before anything is
|
|
129
|
+
* reserved: an adult with the buyer's address (or none) must be the buyer, once; other adults are invited by
|
|
130
|
+
* email, up to the limit; the buyer signs for minors. The core stays the authority: it checks again at continue.
|
|
131
|
+
*/
|
|
132
|
+
export function checkWaiverSeats(order: WaiverOrder): { code: string; fields: string[] } | null {
|
|
133
|
+
const fields: string[] = [];
|
|
134
|
+
let buyerSeated = false;
|
|
135
|
+
let emailed = 0;
|
|
136
|
+
for (const [i, a] of order.attendees.entries()) {
|
|
137
|
+
if (a.minor) continue;
|
|
138
|
+
if (norm(a.email ?? order.buyer.email) !== norm(order.buyer.email)) emailed++;
|
|
139
|
+
else if (buyerSeated || norm(a.name) !== norm(order.buyer.name))
|
|
140
|
+
fields.push(seatField(order.rows[i] ?? 0, "email"));
|
|
141
|
+
else buyerSeated = true;
|
|
142
|
+
}
|
|
143
|
+
if (fields.length > 0) return { code: "WAIVER_EMAIL_REQUIRED", fields };
|
|
144
|
+
return emailed > MAX_EMAIL_SIGNERS ? { code: "WAIVER_TOO_MANY_EMAILS", fields: [] } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
/** Messages for the codes a refused booking comes back with. */
|
|
124
148
|
export const WAIVER_MESSAGES: Record<string, string> = {
|
|
125
149
|
BAD_REQUEST: "Please check the highlighted details.",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps what a person typed when a booking is refused (`<BookingWithWaiver>`). The refusal is a redirect back to
|
|
3
|
+
* the page with codes only: personal details never go in a URL, and a server-rendered copy could end up in a page
|
|
4
|
+
* cache. So the browser keeps them: on submit this tab's `sessionStorage` takes the form's values, and the
|
|
5
|
+
* page the refusal lands on puts them back. Any other visit discards them; so does closing the tab. Without
|
|
6
|
+
* JavaScript (or storage) the form comes back empty with its message, as before. No text; no imports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The parts of a form control this needs. */
|
|
10
|
+
export interface KeptField {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
value: string;
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const KEPT_MS = 15 * 60_000;
|
|
18
|
+
export const keptKey = (eventId: string) => `semics-waiver-form:${eventId}`;
|
|
19
|
+
|
|
20
|
+
type Stored = Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
21
|
+
|
|
22
|
+
/** What to keep: what the person chose, never hidden fields (the idempotency key, the security check's token). */
|
|
23
|
+
export function collectFields(fields: Iterable<KeptField>): Record<string, string | boolean> {
|
|
24
|
+
const out: Record<string, string | boolean> = {};
|
|
25
|
+
for (const f of fields) {
|
|
26
|
+
if (!f.name || f.type === "hidden" || f.type === "submit" || f.type === "button") continue;
|
|
27
|
+
if (f.type === "checkbox") {
|
|
28
|
+
if (f.checked) out[f.name] = true;
|
|
29
|
+
} else if (f.value) out[f.name] = f.value;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyFields(fields: Iterable<KeptField>, kept: Record<string, string | boolean>): void {
|
|
35
|
+
for (const f of fields) {
|
|
36
|
+
const value = kept[f.name];
|
|
37
|
+
if (value === undefined || f.type === "hidden") continue;
|
|
38
|
+
if (f.type === "checkbox") f.checked = value === true;
|
|
39
|
+
else if (typeof value === "string") f.value = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function saveKept(storage: Stored, eventId: string, fields: Iterable<KeptField>, now: number): void {
|
|
44
|
+
try {
|
|
45
|
+
storage.setItem(keptKey(eventId), JSON.stringify({ at: now, values: collectFields(fields) }));
|
|
46
|
+
} catch {
|
|
47
|
+
// Private browsing, a full quota: the form just comes back empty.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The values to restore: only after a refusal of this event's form, once, and only while fresh. */
|
|
52
|
+
export function takeKept(storage: Stored, eventId: string, search: URLSearchParams, now: number) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = storage.getItem(keptKey(eventId));
|
|
55
|
+
storage.removeItem(keptKey(eventId));
|
|
56
|
+
if (!raw || !search.get("waiver_error") || search.get("waiver_event") !== eventId) return null;
|
|
57
|
+
const { at, values } = JSON.parse(raw) as { at: number; values: Record<string, string | boolean> };
|
|
58
|
+
return now - at <= KEPT_MS ? values : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Wires every booking form on the page. Run before the picker's script, which shows the date the select holds. */
|
|
65
|
+
export function keepWaiverForms(doc: Document, storage: Stored, url: URL): void {
|
|
66
|
+
for (const form of doc.querySelectorAll<HTMLFormElement>("form[data-keep-values]")) {
|
|
67
|
+
const eventId = (form.elements.namedItem("eventId") as HTMLInputElement | null)?.value ?? "";
|
|
68
|
+
const fields = () => [...form.elements] as unknown as HTMLInputElement[];
|
|
69
|
+
const kept = takeKept(storage, eventId, url.searchParams, Date.now());
|
|
70
|
+
if (kept) applyFields(fields(), kept);
|
|
71
|
+
form.addEventListener("submit", () => saveKept(storage, eventId, fields(), Date.now()));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof document !== "undefined" && typeof sessionStorage !== "undefined") {
|
|
76
|
+
keepWaiverForms(document, sessionStorage, new URL(location.href));
|
|
77
|
+
}
|
package/src/astro/virtual.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ declare module "virtual:semics-events-suite/config" {
|
|
|
4
4
|
export const eventsPluginId: string;
|
|
5
5
|
/** The E-sign core's plugin id on this site. */
|
|
6
6
|
export const esignPluginId: string;
|
|
7
|
+
/** How the site dresses the suite's page (`eventsSuite({ page })`); checked by Events' integration. */
|
|
8
|
+
export const page: import("@semics-tech/emdash-events/astro/lib/page").PageOptions;
|
|
7
9
|
}
|