@seatlayer/js 0.36.2 → 0.36.3
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/index.cjs +594 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -2
- package/dist/index.d.ts +233 -2
- package/dist/index.js +589 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
|
|
|
38
38
|
// src/index.ts
|
|
39
39
|
var index_exports = {};
|
|
40
40
|
__export(index_exports, {
|
|
41
|
+
ACCESS_LINK_DEFAULTS: () => ACCESS_LINK_DEFAULTS,
|
|
41
42
|
ApiError: () => ApiError,
|
|
42
43
|
BuyerAccessContext: () => BuyerAccessContext,
|
|
43
44
|
BuyerAccessUnavailableError: () => BuyerAccessUnavailableError,
|
|
@@ -53,6 +54,10 @@ __export(index_exports, {
|
|
|
53
54
|
SeatingChart: () => SeatingChart,
|
|
54
55
|
accessIntentLabel: () => accessIntentLabel,
|
|
55
56
|
accessLine: () => accessLine,
|
|
57
|
+
accessLinkBadge: () => accessLinkBadge,
|
|
58
|
+
accessLinkErrorCopy: () => accessLinkErrorCopy,
|
|
59
|
+
accessLinkIsLive: () => accessLinkIsLive,
|
|
60
|
+
accessLinkPolicyLines: () => accessLinkPolicyLines,
|
|
56
61
|
attachPickerFrame: () => attachPickerFrame,
|
|
57
62
|
bucketRows: () => bucketRows,
|
|
58
63
|
bucketRowsHtml: () => bucketRowsHtml,
|
|
@@ -6206,6 +6211,78 @@ function accessLine(access) {
|
|
|
6206
6211
|
function accessIntentLabel(intent) {
|
|
6207
6212
|
return intent === "internal" ? "Internal selling \u2014 our own staff sell these" : intent === "server" ? "Server integration \u2014 our backend lets buyers in" : intent === "hosted_link" ? "Hosted access link \u2014 SeatLayer issues the link" : "No buyer access yet \u2014 the allocation is just protected";
|
|
6208
6213
|
}
|
|
6214
|
+
var ACCESS_LINK_DEFAULTS = {
|
|
6215
|
+
maxRedemptions: 100,
|
|
6216
|
+
maxQuantity: 4
|
|
6217
|
+
};
|
|
6218
|
+
function accessLinkBadge(link) {
|
|
6219
|
+
switch (link.status ?? link.state) {
|
|
6220
|
+
case "active":
|
|
6221
|
+
return { text: "Active", kind: "active" };
|
|
6222
|
+
case "expired":
|
|
6223
|
+
return { text: "Expired", kind: "archived" };
|
|
6224
|
+
case "exhausted":
|
|
6225
|
+
return { text: "All used", kind: "paused" };
|
|
6226
|
+
case "rotated":
|
|
6227
|
+
return { text: "Replaced", kind: "archived" };
|
|
6228
|
+
default:
|
|
6229
|
+
return { text: "Revoked", kind: "archived" };
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
function accessLinkIsLive(link) {
|
|
6233
|
+
return link.state === "active" && link.status === "active";
|
|
6234
|
+
}
|
|
6235
|
+
function formatMoment(ms) {
|
|
6236
|
+
if (!Number.isFinite(ms)) return "\u2014";
|
|
6237
|
+
return new Date(ms).toLocaleString(void 0, {
|
|
6238
|
+
day: "numeric",
|
|
6239
|
+
month: "short",
|
|
6240
|
+
year: "numeric",
|
|
6241
|
+
hour: "numeric",
|
|
6242
|
+
minute: "2-digit"
|
|
6243
|
+
});
|
|
6244
|
+
}
|
|
6245
|
+
function accessLinkPolicyLines(link) {
|
|
6246
|
+
return [
|
|
6247
|
+
{ k: "Expires", v: formatMoment(link.expiresAt) },
|
|
6248
|
+
{
|
|
6249
|
+
k: "Redemptions",
|
|
6250
|
+
v: `${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} used`
|
|
6251
|
+
},
|
|
6252
|
+
{
|
|
6253
|
+
k: "Seats per buyer",
|
|
6254
|
+
v: `${link.maxQuantity.toLocaleString()} seat${link.maxQuantity === 1 ? "" : "s"} maximum`
|
|
6255
|
+
},
|
|
6256
|
+
{
|
|
6257
|
+
k: "Covers",
|
|
6258
|
+
v: link.includePublic ? "This channel's allocation and Public sale seats" : "This channel's allocation only"
|
|
6259
|
+
}
|
|
6260
|
+
];
|
|
6261
|
+
}
|
|
6262
|
+
function accessLinkErrorCopy(err) {
|
|
6263
|
+
const fromServer = err?.serverMessage?.trim();
|
|
6264
|
+
switch (err?.code) {
|
|
6265
|
+
case "invalid_expiry":
|
|
6266
|
+
case "invalid_max_redemptions":
|
|
6267
|
+
case "invalid_max_quantity":
|
|
6268
|
+
case "invalid_session_ttl":
|
|
6269
|
+
case "invalid_label":
|
|
6270
|
+
return fromServer || "That setting is outside what a hosted link allows. Adjust it and try again.";
|
|
6271
|
+
case "too_many_access_links":
|
|
6272
|
+
return fromServer || "This channel already has as many live links as it can hold. Revoke one before creating another.";
|
|
6273
|
+
case "access_link_not_active":
|
|
6274
|
+
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6275
|
+
case "channel_unavailable":
|
|
6276
|
+
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
6277
|
+
case "end_active_sessions_required":
|
|
6278
|
+
return "Choose what happens to the buyers who already came in through this link.";
|
|
6279
|
+
case "not_found":
|
|
6280
|
+
return "That link is no longer here. Refresh and try again.";
|
|
6281
|
+
default:
|
|
6282
|
+
if (err?.status === 403) return "Hosted access links need channel-management permission.";
|
|
6283
|
+
return fromServer || "That did not go through. Try again.";
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6209
6286
|
function dropReviewRows(details) {
|
|
6210
6287
|
return (details?.channels ?? []).map((channel) => ({
|
|
6211
6288
|
kind: "skip",
|
|
@@ -6222,13 +6299,14 @@ function stateBadge(state) {
|
|
|
6222
6299
|
|
|
6223
6300
|
// src/manageApi.ts
|
|
6224
6301
|
var ManageApiError = class extends Error {
|
|
6225
|
-
constructor(status, message, code, conflicts, details) {
|
|
6302
|
+
constructor(status, message, code, conflicts, details, serverMessage) {
|
|
6226
6303
|
super(message);
|
|
6227
6304
|
this.name = "ManageApiError";
|
|
6228
6305
|
this.status = status;
|
|
6229
6306
|
this.code = code;
|
|
6230
6307
|
this.conflicts = conflicts;
|
|
6231
6308
|
this.details = details;
|
|
6309
|
+
this.serverMessage = serverMessage;
|
|
6232
6310
|
}
|
|
6233
6311
|
};
|
|
6234
6312
|
async function parse(res) {
|
|
@@ -6241,7 +6319,8 @@ async function parse(res) {
|
|
|
6241
6319
|
err?.error ?? `request_failed_${res.status}`,
|
|
6242
6320
|
err?.code,
|
|
6243
6321
|
err?.conflicts,
|
|
6244
|
-
err?.details
|
|
6322
|
+
err?.details,
|
|
6323
|
+
typeof err?.message === "string" ? err.message : void 0
|
|
6245
6324
|
);
|
|
6246
6325
|
}
|
|
6247
6326
|
return data;
|
|
@@ -6437,6 +6516,58 @@ var ManageApi = class {
|
|
|
6437
6516
|
body: { accessIntent }
|
|
6438
6517
|
});
|
|
6439
6518
|
}
|
|
6519
|
+
// ---- hosted access links (M8) ----
|
|
6520
|
+
/**
|
|
6521
|
+
* Mint a hosted access link. The 201 is the ONE and ONLY time `url` and
|
|
6522
|
+
* `capability` exist outside the buyer's browser — SeatLayer keeps a hash, so
|
|
6523
|
+
* there is no route, cache, or support escalation that can produce this string
|
|
6524
|
+
* again. Callers must reveal it immediately and then let it go.
|
|
6525
|
+
*
|
|
6526
|
+
* Every omitted field takes the server's default: expiry = when the event
|
|
6527
|
+
* starts, 100 redemptions, 4 seats per buyer, this channel's allocation only.
|
|
6528
|
+
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
6529
|
+
* spelled out in `ManageApiError.serverMessage`.
|
|
6530
|
+
*
|
|
6531
|
+
* Side effect by design: this also declares the channel's access intent as
|
|
6532
|
+
* `hosted_link`, so the rail stops saying "no buyer access configured".
|
|
6533
|
+
*/
|
|
6534
|
+
createAccessLink(key, channelId, input = {}) {
|
|
6535
|
+
return this.auth(
|
|
6536
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`,
|
|
6537
|
+
{ method: "POST", body: input }
|
|
6538
|
+
);
|
|
6539
|
+
}
|
|
6540
|
+
/** Status only — label, expiry, redemptions, per-buyer cap, lineage, and the
|
|
6541
|
+
* live session count. Never the url, never the capability. Needs `:view`. */
|
|
6542
|
+
accessLinks(key, channelId) {
|
|
6543
|
+
return this.auth(
|
|
6544
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
/**
|
|
6548
|
+
* Rotate — the ONLY recovery for a link nobody kept. The old URL stops opening
|
|
6549
|
+
* immediately and the response is a fresh one-time reveal.
|
|
6550
|
+
*
|
|
6551
|
+
* `endActiveSessions` is REQUIRED, not defaulted: the organizer must say
|
|
6552
|
+
* whether buyers already inside finish their checkout or lose access now. The
|
|
6553
|
+
* server answers 422 `end_active_sessions_required` if it is omitted, and that
|
|
6554
|
+
* refusal is correct — a UI must not pick either branch on their behalf.
|
|
6555
|
+
*/
|
|
6556
|
+
rotateAccessLink(key, channelId, linkId, endActiveSessions) {
|
|
6557
|
+
return this.auth(
|
|
6558
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}/rotate`,
|
|
6559
|
+
{ method: "POST", body: { endActiveSessions } }
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
/** Revoke. The link stops opening immediately; `endActiveSessions` decides
|
|
6563
|
+
* whether the buyers already inside keep their sessions. */
|
|
6564
|
+
revokeAccessLink(key, channelId, linkId, endActiveSessions = false) {
|
|
6565
|
+
const qs = endActiveSessions ? "?endActiveSessions=1" : "";
|
|
6566
|
+
return this.auth(
|
|
6567
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}${qs}`,
|
|
6568
|
+
{ method: "DELETE" }
|
|
6569
|
+
);
|
|
6570
|
+
}
|
|
6440
6571
|
// ---- reports (token) ----
|
|
6441
6572
|
report(key) {
|
|
6442
6573
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -6589,6 +6720,26 @@ var CHANNELS_CSS = `
|
|
|
6589
6720
|
border-radius:10px;background:rgba(244,183,64,.06);font-family:ui-monospace,Menlo,monospace;font-size:11px;
|
|
6590
6721
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
6591
6722
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
6723
|
+
|
|
6724
|
+
/* hosted access links \u2014 STATUS only; there is no Copy control on this card */
|
|
6725
|
+
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6726
|
+
margin-bottom:8px}
|
|
6727
|
+
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
6728
|
+
.slm-ch-link .lk-name{flex:1;min-width:0;font-size:12.5px;font-weight:800;overflow:hidden;text-overflow:ellipsis;
|
|
6729
|
+
white-space:nowrap}
|
|
6730
|
+
.slm-ch-lkrow{display:flex;gap:8px;margin-top:5px;font-size:11px;color:var(--slm-muted)}
|
|
6731
|
+
.slm-ch-lkrow .k{flex:none;min-width:104px}
|
|
6732
|
+
.slm-ch-lkrow .v{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
6733
|
+
.slm-ch-meter{height:5px;border-radius:3px;background:rgba(255,255,255,.09);overflow:hidden;margin-top:8px}
|
|
6734
|
+
.slm-ch-meter i{display:block;height:100%;background:var(--slm-accent);
|
|
6735
|
+
transition:width var(--slm-mo-base) var(--slm-mo-out)}
|
|
6736
|
+
.slm-ch-radio{display:flex;gap:9px;align-items:flex-start;padding:11px 12px;border:1px solid var(--slm-line);
|
|
6737
|
+
border-radius:10px;margin-top:8px;font-size:12.5px;cursor:pointer;
|
|
6738
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
6739
|
+
.slm-ch-radio:hover{border-color:var(--slm-muted)}
|
|
6740
|
+
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
6741
|
+
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
6742
|
+
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
6592
6743
|
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;
|
|
6593
6744
|
background:var(--slm-surface);margin-top:10px}
|
|
6594
6745
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
@@ -6621,7 +6772,8 @@ var CHANNELS_CSS = `
|
|
|
6621
6772
|
.slm.compact .slm-modes{display:none}
|
|
6622
6773
|
|
|
6623
6774
|
@media (prefers-reduced-motion:reduce){
|
|
6624
|
-
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail
|
|
6775
|
+
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail,
|
|
6776
|
+
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
6625
6777
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
6626
6778
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
6627
6779
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
@@ -6637,9 +6789,37 @@ function bucketRowsHtml(rows) {
|
|
|
6637
6789
|
${row.peek ? `<span class="peek">${esc(row.peek)}</span>` : "<span></span>"}
|
|
6638
6790
|
</div>`).join("");
|
|
6639
6791
|
}
|
|
6792
|
+
var SERVER_INTEGRATION_HTML = `
|
|
6793
|
+
<p class="slm-eyebrow" style="margin-top:18px">Server integration</p>
|
|
6794
|
+
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
6795
|
+
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
6796
|
+
on its own never grants access.</p>
|
|
6797
|
+
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
6798
|
+
target="_blank" rel="noreferrer noopener">Read the server integration guide \u2192</a></p>`;
|
|
6640
6799
|
function esc(value) {
|
|
6641
6800
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6642
6801
|
}
|
|
6802
|
+
function datetimeLocalValue(ms) {
|
|
6803
|
+
const local = new Date(ms - new Date(ms).getTimezoneOffset() * 6e4);
|
|
6804
|
+
return local.toISOString().slice(0, 16);
|
|
6805
|
+
}
|
|
6806
|
+
function intField(root, selector) {
|
|
6807
|
+
const raw = root.querySelector(selector)?.value.trim() ?? "";
|
|
6808
|
+
const value = Number(raw);
|
|
6809
|
+
return raw !== "" && Number.isInteger(value) ? value : null;
|
|
6810
|
+
}
|
|
6811
|
+
function selectSecret(dialog) {
|
|
6812
|
+
const node = dialog.querySelector("[data-ch-lk-url]");
|
|
6813
|
+
if (!node) return;
|
|
6814
|
+
try {
|
|
6815
|
+
const range = document.createRange();
|
|
6816
|
+
range.selectNodeContents(node);
|
|
6817
|
+
const selection = window.getSelection();
|
|
6818
|
+
selection?.removeAllRanges();
|
|
6819
|
+
selection?.addRange(range);
|
|
6820
|
+
} catch {
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6643
6823
|
var ChannelsMode = class {
|
|
6644
6824
|
constructor(host, capabilities) {
|
|
6645
6825
|
this.active = false;
|
|
@@ -6656,6 +6836,15 @@ var ChannelsMode = class {
|
|
|
6656
6836
|
this.dialog = null;
|
|
6657
6837
|
this.detent = "medium";
|
|
6658
6838
|
this.seatListLimit = SEAT_LIST_PAGE;
|
|
6839
|
+
/**
|
|
6840
|
+
* Hosted-link STATUS for the channel whose detail panel is open. This is the
|
|
6841
|
+
* listing projection — it carries no url and no capability, because no route
|
|
6842
|
+
* returns one. `unsupported` is the honest answer for a worker that predates
|
|
6843
|
+
* M8, exactly like the buyer-preview probe.
|
|
6844
|
+
*/
|
|
6845
|
+
this.links = [];
|
|
6846
|
+
this.linksChannelId = null;
|
|
6847
|
+
this.linksState = "idle";
|
|
6659
6848
|
this.previewAudience = [];
|
|
6660
6849
|
this.previewIncludePublic = false;
|
|
6661
6850
|
this.previewProjection = null;
|
|
@@ -6699,6 +6888,9 @@ var ChannelsMode = class {
|
|
|
6699
6888
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
6700
6889
|
this.pollTimer = null;
|
|
6701
6890
|
this.closeDialog({ restoreFocus: false });
|
|
6891
|
+
this.links = [];
|
|
6892
|
+
this.linksChannelId = null;
|
|
6893
|
+
this.linksState = "idle";
|
|
6702
6894
|
this.layer?.classList.remove("on");
|
|
6703
6895
|
this.host.root.classList.remove(
|
|
6704
6896
|
"ch-mode",
|
|
@@ -6770,6 +6962,7 @@ var ChannelsMode = class {
|
|
|
6770
6962
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6771
6963
|
}
|
|
6772
6964
|
await this.loadAllocation();
|
|
6965
|
+
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
6773
6966
|
this.loading = false;
|
|
6774
6967
|
if (this.active) {
|
|
6775
6968
|
this.paintRail();
|
|
@@ -7207,11 +7400,8 @@ var ChannelsMode = class {
|
|
|
7207
7400
|
<p class="slm-hint">${channel.access?.hasActiveGrants ? "Buyer access is live. Only this audience can buy from the allocation." : "No buyer access is configured yet. The allocation is protected \u2014 it is not available to Public sale."}</p>
|
|
7208
7401
|
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
7209
7402
|
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
<div class="slm-ch-row2"><button type="button" class="slm-btn ghost" data-ch-act="server-access" disabled
|
|
7213
|
-
title="Guided server setup ships in the next milestone">Configure server integration \xB7 Coming soon</button></div>
|
|
7214
|
-
<p class="slm-note">Your own server can already mint buyer access sessions for this channel with the server SDK.</p>` : "";
|
|
7403
|
+
${this.hostedLinksHtml()}
|
|
7404
|
+
${SERVER_INTEGRATION_HTML}` : "";
|
|
7215
7405
|
return `
|
|
7216
7406
|
<p class="slm-eyebrow">
|
|
7217
7407
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -7221,6 +7411,90 @@ var ChannelsMode = class {
|
|
|
7221
7411
|
${access}
|
|
7222
7412
|
${lifecycle}`;
|
|
7223
7413
|
}
|
|
7414
|
+
// ---- hosted access links --------------------------------------------------
|
|
7415
|
+
/**
|
|
7416
|
+
* Read the status projection for the open channel. Never paints — the caller
|
|
7417
|
+
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
7418
|
+
* user-driven one. A worker without M8 answers 404/405 and gets the honest
|
|
7419
|
+
* "needs a newer server" line rather than an error toast.
|
|
7420
|
+
*/
|
|
7421
|
+
async loadLinks(channelId) {
|
|
7422
|
+
if (!this.caps.view) return;
|
|
7423
|
+
if (this.linksChannelId !== channelId) {
|
|
7424
|
+
this.links = [];
|
|
7425
|
+
this.linksChannelId = channelId;
|
|
7426
|
+
this.linksState = "loading";
|
|
7427
|
+
}
|
|
7428
|
+
try {
|
|
7429
|
+
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7430
|
+
if (this.linksChannelId !== channelId) return;
|
|
7431
|
+
this.links = res.links ?? [];
|
|
7432
|
+
this.linksState = "ready";
|
|
7433
|
+
} catch (err) {
|
|
7434
|
+
if (this.linksChannelId !== channelId) return;
|
|
7435
|
+
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7436
|
+
this.links = [];
|
|
7437
|
+
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
7438
|
+
if (this.linksState === "error") this.host.onError(err);
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
/**
|
|
7442
|
+
* The hosted-link section of the detail panel.
|
|
7443
|
+
*
|
|
7444
|
+
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
7445
|
+
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
7446
|
+
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
7447
|
+
* be produced again. Rotation is the recovery path, and it says so.
|
|
7448
|
+
*/
|
|
7449
|
+
hostedLinksHtml() {
|
|
7450
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Hosted access links</p>`;
|
|
7451
|
+
if (this.linksState === "unsupported") {
|
|
7452
|
+
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
7453
|
+
<span><b>Hosted links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
7454
|
+
}
|
|
7455
|
+
if (this.linksState === "error") {
|
|
7456
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7457
|
+
<span><b>Couldn't load this channel's links.</b>
|
|
7458
|
+
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
7459
|
+
}
|
|
7460
|
+
const live = this.links.filter(accessLinkIsLive).length;
|
|
7461
|
+
const cards = this.linksState === "loading" && !this.links.length ? `<div class="slm-empty">Loading links\u2026</div>` : this.links.length ? this.links.map((link) => this.linkCardHtml(link)).join("") : `<p class="slm-hint">No hosted link yet. Create one to send this allocation to a named group \u2014
|
|
7462
|
+
they open the link and buy only these seats.</p>`;
|
|
7463
|
+
const create = this.caps.manage ? `<button type="button" class="slm-btn" style="width:100%" data-ch-act="link-create">
|
|
7464
|
+
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
7465
|
+
return `${eyebrow}
|
|
7466
|
+
${cards}
|
|
7467
|
+
${create}
|
|
7468
|
+
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
7469
|
+
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
7470
|
+
}
|
|
7471
|
+
linkCardHtml(link) {
|
|
7472
|
+
const badge = accessLinkBadge(link);
|
|
7473
|
+
const used = link.maxRedemptions > 0 ? Math.min(100, Math.round(link.redemptions / link.maxRedemptions * 100)) : 0;
|
|
7474
|
+
const rows = accessLinkPolicyLines(link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
7475
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
7476
|
+
const sessions = link.activeSessions ? `<div class="slm-ch-lkrow"><span class="k">Buyers inside now</span>
|
|
7477
|
+
<span class="v">${link.activeSessions.toLocaleString()}</span></div>` : "";
|
|
7478
|
+
const lastUsed = link.lastRedeemedAt ? `<div class="slm-ch-lkrow"><span class="k">Last opened</span>
|
|
7479
|
+
<span class="v">${esc(new Date(link.lastRedeemedAt).toLocaleString())}</span></div>` : "";
|
|
7480
|
+
const actions = this.caps.manage && accessLinkIsLive(link) ? `<div class="slm-ch-row2">
|
|
7481
|
+
<button type="button" class="slm-btn ghost" data-ch-rotate="${esc(link.id)}">Rotate</button>
|
|
7482
|
+
<button type="button" class="slm-btn ghost" data-ch-revoke="${esc(link.id)}">Revoke</button>
|
|
7483
|
+
</div>` : "";
|
|
7484
|
+
return `<div class="slm-ch-link">
|
|
7485
|
+
<span class="lk-head">
|
|
7486
|
+
<span class="lk-name">${esc(link.label || "Hosted link")}</span>
|
|
7487
|
+
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
7488
|
+
</span>
|
|
7489
|
+
<div class="slm-ch-meter" role="img"
|
|
7490
|
+
aria-label="${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} redemptions used">
|
|
7491
|
+
<i style="width:${used}%"></i></div>
|
|
7492
|
+
${rows}${sessions}${lastUsed}
|
|
7493
|
+
<div class="slm-ch-lkrow"><span class="k">The URL</span>
|
|
7494
|
+
<span class="v">Revealed once at creation \u2014 not recoverable</span></div>
|
|
7495
|
+
${actions}
|
|
7496
|
+
</div>`;
|
|
7497
|
+
}
|
|
7224
7498
|
previewRailHtml() {
|
|
7225
7499
|
const audienceOptions = [
|
|
7226
7500
|
{ id: PUBLIC_CHANNEL_ID, name: this.list?.publicSale.name ?? PUBLIC_CHANNEL_NAME },
|
|
@@ -7269,10 +7543,26 @@ var ChannelsMode = class {
|
|
|
7269
7543
|
});
|
|
7270
7544
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7271
7545
|
button.addEventListener("click", () => {
|
|
7272
|
-
|
|
7546
|
+
const channelId = button.dataset.chDetail;
|
|
7547
|
+
this.detailChannelId = channelId;
|
|
7273
7548
|
this.paintRail();
|
|
7549
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
7274
7550
|
});
|
|
7275
7551
|
});
|
|
7552
|
+
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
7553
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7554
|
+
kind: "linkRotate",
|
|
7555
|
+
channelId: this.detailChannelId,
|
|
7556
|
+
linkId: button.dataset.chRotate
|
|
7557
|
+
}));
|
|
7558
|
+
});
|
|
7559
|
+
rail.querySelectorAll("[data-ch-revoke]").forEach((button) => {
|
|
7560
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7561
|
+
kind: "linkRevoke",
|
|
7562
|
+
channelId: this.detailChannelId,
|
|
7563
|
+
linkId: button.dataset.chRevoke
|
|
7564
|
+
}));
|
|
7565
|
+
});
|
|
7276
7566
|
const target = rail.querySelector("[data-ch-target]");
|
|
7277
7567
|
target?.addEventListener("change", () => {
|
|
7278
7568
|
this.targetChannelId = target.value;
|
|
@@ -7324,8 +7614,17 @@ var ChannelsMode = class {
|
|
|
7324
7614
|
break;
|
|
7325
7615
|
case "back":
|
|
7326
7616
|
this.detailChannelId = null;
|
|
7617
|
+
this.linksChannelId = null;
|
|
7618
|
+
this.links = [];
|
|
7619
|
+
this.linksState = "idle";
|
|
7327
7620
|
this.paintRail();
|
|
7328
7621
|
break;
|
|
7622
|
+
case "link-create":
|
|
7623
|
+
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
7624
|
+
break;
|
|
7625
|
+
case "link-reload":
|
|
7626
|
+
if (this.detailChannelId) void this.reloadLinks();
|
|
7627
|
+
break;
|
|
7329
7628
|
case "retry":
|
|
7330
7629
|
void this.refresh();
|
|
7331
7630
|
break;
|
|
@@ -7397,6 +7696,9 @@ var ChannelsMode = class {
|
|
|
7397
7696
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
7398
7697
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
7399
7698
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
7699
|
+
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
7700
|
+
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
7701
|
+
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
7400
7702
|
}
|
|
7401
7703
|
/**
|
|
7402
7704
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -7806,6 +8108,284 @@ var ChannelsMode = class {
|
|
|
7806
8108
|
});
|
|
7807
8109
|
});
|
|
7808
8110
|
}
|
|
8111
|
+
// ---- hosted-link dialogs --------------------------------------------------
|
|
8112
|
+
async reloadLinks() {
|
|
8113
|
+
const channelId = this.detailChannelId;
|
|
8114
|
+
if (!channelId) return;
|
|
8115
|
+
this.linksState = this.links.length ? this.linksState : "loading";
|
|
8116
|
+
await this.loadLinks(channelId);
|
|
8117
|
+
this.paintRail();
|
|
8118
|
+
}
|
|
8119
|
+
linkById(linkId) {
|
|
8120
|
+
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8121
|
+
}
|
|
8122
|
+
/**
|
|
8123
|
+
* Create. The three policy fields carry the owner's defaults and every one of
|
|
8124
|
+
* them is editable; the PLATFORM bounds (60s–180d, 1–10 000, 1–100, 20 live
|
|
8125
|
+
* links) are the server's to enforce and the server's to explain, so this form
|
|
8126
|
+
* checks only that a number is a number and surfaces the server's sentence for
|
|
8127
|
+
* everything else.
|
|
8128
|
+
*/
|
|
8129
|
+
renderLinkCreateDialog(state) {
|
|
8130
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8131
|
+
if (!channel || !this.caps.manage) {
|
|
8132
|
+
this.closeDialog();
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
this.renderScrim(`
|
|
8136
|
+
<h3 id="slm-ch-dlg-title">Create a hosted access link for ${esc(channel.name)}</h3>
|
|
8137
|
+
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
8138
|
+
You'll see the link once, right after you create it.</p>
|
|
8139
|
+
<div class="slm-field">
|
|
8140
|
+
<label for="slm-ch-lk-label">Label <span style="text-transform:none;font-weight:500">(optional)</span></label>
|
|
8141
|
+
<input class="slm-input" id="slm-ch-lk-label" maxlength="80" placeholder="e.g. VIP list Nov 14" />
|
|
8142
|
+
<p class="slm-note">So you can tell your links apart later. Buyers never see it.</p>
|
|
8143
|
+
</div>
|
|
8144
|
+
<div class="slm-field">
|
|
8145
|
+
<label for="slm-ch-lk-expiry">Stops working</label>
|
|
8146
|
+
<select class="slm-select" id="slm-ch-lk-expiry" data-ch-lk-expiry>
|
|
8147
|
+
<option value="event" selected>When the event starts</option>
|
|
8148
|
+
<option value="custom">On a date I choose</option>
|
|
8149
|
+
</select>
|
|
8150
|
+
</div>
|
|
8151
|
+
<div class="slm-field" data-ch-lk-when-field hidden>
|
|
8152
|
+
<label for="slm-ch-lk-when">Date and time</label>
|
|
8153
|
+
<input class="slm-input" type="datetime-local" id="slm-ch-lk-when" />
|
|
8154
|
+
</div>
|
|
8155
|
+
<div class="slm-field">
|
|
8156
|
+
<label for="slm-ch-lk-redemptions">How many people can use it</label>
|
|
8157
|
+
<input class="slm-input" type="number" id="slm-ch-lk-redemptions" inputmode="numeric"
|
|
8158
|
+
value="${ACCESS_LINK_DEFAULTS.maxRedemptions}" />
|
|
8159
|
+
<p class="slm-note">Each buyer who opens the link uses one.</p>
|
|
8160
|
+
</div>
|
|
8161
|
+
<div class="slm-field">
|
|
8162
|
+
<label for="slm-ch-lk-quantity">Seats per buyer</label>
|
|
8163
|
+
<input class="slm-input" type="number" id="slm-ch-lk-quantity" inputmode="numeric"
|
|
8164
|
+
value="${ACCESS_LINK_DEFAULTS.maxQuantity}" />
|
|
8165
|
+
</div>
|
|
8166
|
+
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:2px 0 6px">
|
|
8167
|
+
<input type="checkbox" id="slm-ch-lk-public" />
|
|
8168
|
+
Also let this link buy Public sale seats
|
|
8169
|
+
</label>
|
|
8170
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8171
|
+
<div class="foot">
|
|
8172
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8173
|
+
<button type="button" class="slm-btn" data-ch-lk-create>Create link</button>
|
|
8174
|
+
</div>`, (dialog) => {
|
|
8175
|
+
const expiry = dialog.querySelector("[data-ch-lk-expiry]");
|
|
8176
|
+
const whenField = dialog.querySelector("[data-ch-lk-when-field]");
|
|
8177
|
+
const when = dialog.querySelector("#slm-ch-lk-when");
|
|
8178
|
+
expiry.addEventListener("change", () => {
|
|
8179
|
+
const custom = expiry.value === "custom";
|
|
8180
|
+
whenField.hidden = !custom;
|
|
8181
|
+
if (custom && !when.value) when.value = datetimeLocalValue(Date.now() + 7 * 864e5);
|
|
8182
|
+
});
|
|
8183
|
+
dialog.querySelector("[data-ch-lk-create]")?.addEventListener("click", () => {
|
|
8184
|
+
const maxRedemptions = intField(dialog, "#slm-ch-lk-redemptions");
|
|
8185
|
+
const maxQuantity = intField(dialog, "#slm-ch-lk-quantity");
|
|
8186
|
+
if (maxRedemptions == null || maxQuantity == null) {
|
|
8187
|
+
this.showDialogError("Those two settings need to be whole numbers.");
|
|
8188
|
+
return;
|
|
8189
|
+
}
|
|
8190
|
+
let expiresAt;
|
|
8191
|
+
if (expiry.value === "custom") {
|
|
8192
|
+
expiresAt = Date.parse(when.value);
|
|
8193
|
+
if (!Number.isFinite(expiresAt)) {
|
|
8194
|
+
this.showDialogError("Pick the date and time the link should stop working.");
|
|
8195
|
+
return;
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
8198
|
+
void this.createLink(channel.id, {
|
|
8199
|
+
label: dialog.querySelector("#slm-ch-lk-label")?.value.trim() || null,
|
|
8200
|
+
includePublic: dialog.querySelector("#slm-ch-lk-public")?.checked ?? false,
|
|
8201
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
8202
|
+
maxRedemptions,
|
|
8203
|
+
maxQuantity
|
|
8204
|
+
});
|
|
8205
|
+
});
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
async createLink(channelId, input) {
|
|
8209
|
+
try {
|
|
8210
|
+
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8211
|
+
this.revealLink(reveal, { channelId });
|
|
8212
|
+
await this.refresh({ quiet: true });
|
|
8213
|
+
} catch (err) {
|
|
8214
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8215
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
/**
|
|
8219
|
+
* The ONE-TIME reveal.
|
|
8220
|
+
*
|
|
8221
|
+
* Three things make this unrecoverable rather than merely "not shown twice":
|
|
8222
|
+
*
|
|
8223
|
+
* 1. `url` is a local const. It is never assigned to a field on this class,
|
|
8224
|
+
* never handed to the host, never put in a `DialogState`.
|
|
8225
|
+
* 2. `this.dialog` is cleared FIRST, so `renderDialog()` — the only function
|
|
8226
|
+
* that rebuilds a sheet — has nothing to rebuild this one from.
|
|
8227
|
+
* 3. The string exists in exactly one DOM node inside the scrim. Dismissing
|
|
8228
|
+
* the dialog removes the scrim, and the closure goes with it.
|
|
8229
|
+
*
|
|
8230
|
+
* The server holds only a hash, so even a compromised client cannot ask for it
|
|
8231
|
+
* again. Rotation is the recovery path, and the copy says so.
|
|
8232
|
+
*/
|
|
8233
|
+
revealLink(reveal, opts) {
|
|
8234
|
+
const url = reveal.url;
|
|
8235
|
+
this.dialog = null;
|
|
8236
|
+
const rotated = opts.rotated ? `<div class="slm-ch-alert warn" role="status"><span>\u26A0</span><span>
|
|
8237
|
+
<b>The old link has stopped working.</b> ${reveal.endedSessions ? `${reveal.endedSessions.toLocaleString()} buyer${reveal.endedSessions === 1 ? "" : "s"} lost access immediately.` : "Buyers who already came in can finish; every new visit needs this link."}</span></div>` : "";
|
|
8238
|
+
const policy = accessLinkPolicyLines(reveal.link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
8239
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
8240
|
+
this.renderScrim(`
|
|
8241
|
+
<h3 id="slm-ch-dlg-title">Copy this link now</h3>
|
|
8242
|
+
<p class="sub">This is the only time SeatLayer can show it. We keep just a fingerprint, so it cannot be
|
|
8243
|
+
shown again \u2014 if it is lost, rotate the link for a fresh one.</p>
|
|
8244
|
+
${rotated}
|
|
8245
|
+
<div class="slm-ch-secret" data-ch-lk-url>${esc(url)}</div>
|
|
8246
|
+
<div class="slm-ch-row2" style="margin-top:8px">
|
|
8247
|
+
<button type="button" class="slm-btn" data-ch-lk-copy>Copy link</button>
|
|
8248
|
+
</div>
|
|
8249
|
+
<div class="slm-ch-alert warn" style="margin-top:12px"><span>\u26A0</span>
|
|
8250
|
+
<span>Anyone who opens this link can buy from this allocation. Send it only to the people it is meant
|
|
8251
|
+
for \u2014 forwarding it hands on the same access, and SeatLayer cannot tell the difference.</span></div>
|
|
8252
|
+
<p class="slm-eyebrow" style="margin-top:14px">What this link allows</p>
|
|
8253
|
+
${policy}
|
|
8254
|
+
<div class="foot">
|
|
8255
|
+
<button type="button" class="slm-btn" data-ch-close data-ch-lk-done>I've copied it</button>
|
|
8256
|
+
</div>`, (dialog) => {
|
|
8257
|
+
const copy = dialog.querySelector("[data-ch-lk-copy]");
|
|
8258
|
+
copy?.addEventListener("click", () => {
|
|
8259
|
+
const ok = () => {
|
|
8260
|
+
copy.textContent = "Copied";
|
|
8261
|
+
this.announce("Hosted access link copied.");
|
|
8262
|
+
};
|
|
8263
|
+
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
8264
|
+
if (clipboard?.writeText) {
|
|
8265
|
+
clipboard.writeText(url).then(ok, () => selectSecret(dialog));
|
|
8266
|
+
return;
|
|
8267
|
+
}
|
|
8268
|
+
selectSecret(dialog);
|
|
8269
|
+
});
|
|
8270
|
+
dialog.querySelector("[data-ch-lk-done]")?.addEventListener("click", () => {
|
|
8271
|
+
void this.loadLinks(opts.channelId).then(() => this.paintRail());
|
|
8272
|
+
});
|
|
8273
|
+
});
|
|
8274
|
+
this.announce("Your hosted access link is ready and is shown once.");
|
|
8275
|
+
}
|
|
8276
|
+
/**
|
|
8277
|
+
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
8278
|
+
* the confirm stays disabled until one of the two choices is picked, because
|
|
8279
|
+
* the gentle branch and the destructive branch are both real decisions and the
|
|
8280
|
+
* server refuses (422 `end_active_sessions_required`) to guess either.
|
|
8281
|
+
*/
|
|
8282
|
+
renderLinkRotateDialog(state) {
|
|
8283
|
+
const link = this.linkById(state.linkId);
|
|
8284
|
+
if (!link || !this.caps.manage) {
|
|
8285
|
+
this.closeDialog();
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
8288
|
+
const sessions = link.activeSessions ?? 0;
|
|
8289
|
+
const warning = sessions ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
8290
|
+
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
8291
|
+
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
8292
|
+
this.renderScrim(`
|
|
8293
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "hosted")} link?</h3>
|
|
8294
|
+
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
8295
|
+
link to copy \u2014 shown once.</p>
|
|
8296
|
+
${warning}
|
|
8297
|
+
<label class="slm-ch-radio">
|
|
8298
|
+
<input type="radio" name="slm-ch-rot" value="keep" data-ch-rot />
|
|
8299
|
+
<span><b>Let them finish</b><span class="why">Access already handed out expires on its own; seats in
|
|
8300
|
+
checkout are untouched. Every new visit needs the new link.</span></span>
|
|
8301
|
+
</label>
|
|
8302
|
+
<label class="slm-ch-radio">
|
|
8303
|
+
<input type="radio" name="slm-ch-rot" value="end" data-ch-rot />
|
|
8304
|
+
<span><b>End their access now</b><span class="why">All access from the old link ends immediately.
|
|
8305
|
+
Buyers part-way through choosing seats lose access.</span></span>
|
|
8306
|
+
</label>
|
|
8307
|
+
<p class="slm-note">Choose one \u2014 SeatLayer will not decide this for you.</p>
|
|
8308
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8309
|
+
<div class="foot">
|
|
8310
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8311
|
+
<button type="button" class="slm-btn" data-ch-lk-rotate disabled>Rotate and copy new link</button>
|
|
8312
|
+
</div>`, (dialog) => {
|
|
8313
|
+
const confirm = dialog.querySelector("[data-ch-lk-rotate]");
|
|
8314
|
+
dialog.querySelectorAll("[data-ch-rot]").forEach((radio) => {
|
|
8315
|
+
radio.addEventListener("change", () => {
|
|
8316
|
+
confirm.disabled = false;
|
|
8317
|
+
});
|
|
8318
|
+
});
|
|
8319
|
+
confirm.addEventListener("click", () => {
|
|
8320
|
+
const picked = [...dialog.querySelectorAll("[data-ch-rot]")].find((radio) => radio.checked);
|
|
8321
|
+
if (!picked) {
|
|
8322
|
+
this.showDialogError(accessLinkErrorCopy({ code: "end_active_sessions_required" }));
|
|
8323
|
+
return;
|
|
8324
|
+
}
|
|
8325
|
+
void this.rotateLink(state.channelId, link.id, picked.value === "end");
|
|
8326
|
+
});
|
|
8327
|
+
});
|
|
8328
|
+
}
|
|
8329
|
+
async rotateLink(channelId, linkId, endActiveSessions) {
|
|
8330
|
+
try {
|
|
8331
|
+
const reveal = await this.host.api.rotateAccessLink(
|
|
8332
|
+
this.host.eventKey,
|
|
8333
|
+
channelId,
|
|
8334
|
+
linkId,
|
|
8335
|
+
endActiveSessions
|
|
8336
|
+
);
|
|
8337
|
+
this.revealLink(reveal, { channelId, rotated: true });
|
|
8338
|
+
} catch (err) {
|
|
8339
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8340
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
renderLinkRevokeDialog(state) {
|
|
8344
|
+
const link = this.linkById(state.linkId);
|
|
8345
|
+
if (!link || !this.caps.manage) {
|
|
8346
|
+
this.closeDialog();
|
|
8347
|
+
return;
|
|
8348
|
+
}
|
|
8349
|
+
const sessions = link.activeSessions ?? 0;
|
|
8350
|
+
this.renderScrim(`
|
|
8351
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "hosted")} link?</h3>
|
|
8352
|
+
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
8353
|
+
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
8354
|
+
${sessions ? `<label class="slm-ch-radio">
|
|
8355
|
+
<input type="checkbox" data-ch-lk-endsessions />
|
|
8356
|
+
<span><b>Also end access for the ${sessions.toLocaleString()}
|
|
8357
|
+
buyer${sessions === 1 ? "" : "s"} already inside</b><span class="why">Leave this off and they can
|
|
8358
|
+
finish what they started; new visits are refused either way.</span></span>
|
|
8359
|
+
</label>` : ""}
|
|
8360
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8361
|
+
<div class="foot">
|
|
8362
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8363
|
+
<button type="button" class="slm-btn danger" data-ch-lk-revoke>Revoke link</button>
|
|
8364
|
+
</div>`, (dialog) => {
|
|
8365
|
+
dialog.querySelector("[data-ch-lk-revoke]")?.addEventListener("click", () => {
|
|
8366
|
+
const end = dialog.querySelector("[data-ch-lk-endsessions]")?.checked ?? false;
|
|
8367
|
+
void this.revokeLink(state.channelId, link.id, end);
|
|
8368
|
+
});
|
|
8369
|
+
});
|
|
8370
|
+
}
|
|
8371
|
+
async revokeLink(channelId, linkId, endActiveSessions) {
|
|
8372
|
+
try {
|
|
8373
|
+
const res = await this.host.api.revokeAccessLink(
|
|
8374
|
+
this.host.eventKey,
|
|
8375
|
+
channelId,
|
|
8376
|
+
linkId,
|
|
8377
|
+
endActiveSessions
|
|
8378
|
+
);
|
|
8379
|
+
this.closeDialog();
|
|
8380
|
+
await this.loadLinks(channelId);
|
|
8381
|
+
await this.refresh({ quiet: true });
|
|
8382
|
+
this.paintRail();
|
|
8383
|
+
this.host.toast(res.endedSessions ? `Link revoked. ${res.endedSessions.toLocaleString()} buyer${res.endedSessions === 1 ? "" : "s"} lost access.` : "Link revoked. It no longer opens for anyone.", "ok");
|
|
8384
|
+
} catch (err) {
|
|
8385
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8386
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
7809
8389
|
// ---- compact detents ------------------------------------------------------
|
|
7810
8390
|
applySheetClasses() {
|
|
7811
8391
|
const root = this.host.root;
|
|
@@ -10023,6 +10603,7 @@ var SeatManager = class {
|
|
|
10023
10603
|
};
|
|
10024
10604
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10025
10605
|
0 && (module.exports = {
|
|
10606
|
+
ACCESS_LINK_DEFAULTS,
|
|
10026
10607
|
ApiError,
|
|
10027
10608
|
BuyerAccessContext,
|
|
10028
10609
|
BuyerAccessUnavailableError,
|
|
@@ -10038,6 +10619,10 @@ var SeatManager = class {
|
|
|
10038
10619
|
SeatingChart,
|
|
10039
10620
|
accessIntentLabel,
|
|
10040
10621
|
accessLine,
|
|
10622
|
+
accessLinkBadge,
|
|
10623
|
+
accessLinkErrorCopy,
|
|
10624
|
+
accessLinkIsLive,
|
|
10625
|
+
accessLinkPolicyLines,
|
|
10041
10626
|
attachPickerFrame,
|
|
10042
10627
|
bucketRows,
|
|
10043
10628
|
bucketRowsHtml,
|