@seatlayer/js 0.44.0 → 0.45.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/dist/index.cjs +317 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -27
- package/dist/index.d.ts +188 -27
- package/dist/index.js +314 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -408,6 +408,7 @@ __export(index_exports, {
|
|
|
408
408
|
SeatManager: () => SeatManager,
|
|
409
409
|
SeatPicker: () => SeatPicker,
|
|
410
410
|
SeatingChart: () => SeatingChart,
|
|
411
|
+
accessIntentDescription: () => accessIntentDescription,
|
|
411
412
|
accessIntentLabel: () => accessIntentLabel,
|
|
412
413
|
accessLine: () => accessLine,
|
|
413
414
|
accessLinkBadge: () => accessLinkBadge,
|
|
@@ -420,6 +421,8 @@ __export(index_exports, {
|
|
|
420
421
|
createBuyerAccessContext: () => createBuyerAccessContext,
|
|
421
422
|
createControllerSink: () => createControllerSink,
|
|
422
423
|
dropReviewRows: () => dropReviewRows,
|
|
424
|
+
intentForbidsCopy: () => intentForbidsCopy,
|
|
425
|
+
intentSwitchBlockedCopy: () => intentSwitchBlockedCopy,
|
|
423
426
|
isPublicChannelId: () => isPublicChannelId,
|
|
424
427
|
markerLetter: () => markerLetter,
|
|
425
428
|
markerOf: () => markerOf,
|
|
@@ -6901,13 +6904,59 @@ function retryAfterCopy(details) {
|
|
|
6901
6904
|
}
|
|
6902
6905
|
function accessLine(access) {
|
|
6903
6906
|
if (!access || !access.intent) return "\u2014";
|
|
6904
|
-
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "
|
|
6907
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : access.intent === "internal" ? "Your staff sell these" : "Protected reserve";
|
|
6905
6908
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6906
6909
|
const detail = access.detail ?? grants;
|
|
6907
6910
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
6908
6911
|
}
|
|
6909
6912
|
function accessIntentLabel(intent) {
|
|
6910
|
-
return intent === "internal" ? "
|
|
6913
|
+
return intent === "internal" ? "Sell through your own staff" : intent === "server" ? "Integrate a website or app" : intent === "hosted_link" ? "Sell with a buyer link" : "Keep as protected reserve";
|
|
6914
|
+
}
|
|
6915
|
+
function accessIntentDescription(intent) {
|
|
6916
|
+
switch (intent) {
|
|
6917
|
+
case "internal":
|
|
6918
|
+
return "Only your own box office can sell these seats, through your secret key. Buyer links and website integrations are refused.";
|
|
6919
|
+
case "server":
|
|
6920
|
+
return "Your website's backend mints each buyer a short-lived session for these seats. Buyer links are refused; the code lives on the Embed page.";
|
|
6921
|
+
case "hosted_link":
|
|
6922
|
+
return "SeatLayer makes a link you send to a named group. They open it and buy only these seats. No other route can sell them.";
|
|
6923
|
+
default:
|
|
6924
|
+
return "Nobody can buy these seats. Every way of letting a buyer in \u2014 a buyer link, your website, even your own staff \u2014 is refused while this is the route. The seats stay out of public sale.";
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
function intentForRoute(route) {
|
|
6928
|
+
return route === "hosted_link" ? "hosted_link" : route === "server" ? "server" : route === "staff" ? "internal" : null;
|
|
6929
|
+
}
|
|
6930
|
+
function intentForbidsCopy(details) {
|
|
6931
|
+
const current = parseIntent(details?.accessIntent);
|
|
6932
|
+
const wanted = intentForRoute(details?.route);
|
|
6933
|
+
const head = `This channel is set to "${accessIntentLabel(current)}"`;
|
|
6934
|
+
return wanted ? `${head}, so it cannot do that. Switch it to "${accessIntentLabel(wanted)}" first.` : `${head}, so it cannot do that. Choose a different route for this channel first.`;
|
|
6935
|
+
}
|
|
6936
|
+
function parseIntent(value) {
|
|
6937
|
+
return value === "internal" || value === "server" || value === "hosted_link" || value === "none" ? value : "none";
|
|
6938
|
+
}
|
|
6939
|
+
function plural(count, one, many) {
|
|
6940
|
+
return `${count.toLocaleString()} ${count === 1 ? one : many}`;
|
|
6941
|
+
}
|
|
6942
|
+
function intentSwitchBlockedCopy(details) {
|
|
6943
|
+
const links = Math.max(0, details?.liveAccessLinks ?? 0);
|
|
6944
|
+
const sessions = Math.max(0, details?.activeSessions ?? 0);
|
|
6945
|
+
const from = parseIntent(details?.from);
|
|
6946
|
+
const to = parseIntent(details?.to);
|
|
6947
|
+
const live = [
|
|
6948
|
+
links ? plural(links, "buyer link is live", "buyer links are live") : null,
|
|
6949
|
+
sessions ? plural(sessions, "buyer is in a checkout", "buyers are in a checkout") : null
|
|
6950
|
+
].filter(Boolean).join(", and ");
|
|
6951
|
+
const headline = `${live || "Buyers are inside this channel"} on "${accessIntentLabel(from)}". Moving it to "${accessIntentLabel(to)}" changes what happens to them.`;
|
|
6952
|
+
const consequences = [];
|
|
6953
|
+
if (links) {
|
|
6954
|
+
consequences.push(`${plural(links, "buyer link closes", "buyer links close")} immediately. Anyone who has not opened it yet never will \u2014 send a new link if you still need one.`);
|
|
6955
|
+
}
|
|
6956
|
+
if (sessions) {
|
|
6957
|
+
consequences.push(`${plural(sessions, "buyer who is already in a checkout keeps", "buyers who are already in a checkout keep")} their seats and can finish paying. Nobody is thrown out. No new buyers come in this way, so the old route empties on its own within 12 hours.`);
|
|
6958
|
+
}
|
|
6959
|
+
return { headline, consequences };
|
|
6911
6960
|
}
|
|
6912
6961
|
var ACCESS_LINK_DEFAULTS = {
|
|
6913
6962
|
maxRedemptions: 100,
|
|
@@ -6972,6 +7021,11 @@ function accessLinkErrorCopy(err) {
|
|
|
6972
7021
|
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6973
7022
|
case "channel_unavailable":
|
|
6974
7023
|
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
7024
|
+
// The channel declares a different sale route. The UI declares `hosted_link`
|
|
7025
|
+
// before it creates, so reaching this means the declaration itself was
|
|
7026
|
+
// refused or raced — say which route is in the way, not the code.
|
|
7027
|
+
case "channel_access_intent_forbids":
|
|
7028
|
+
return intentForbidsCopy(err?.details);
|
|
6975
7029
|
case "end_active_sessions_required":
|
|
6976
7030
|
return "Choose what happens to the buyers who already came in through this link.";
|
|
6977
7031
|
case "not_found":
|
|
@@ -7205,13 +7259,31 @@ var ManageApi = class {
|
|
|
7205
7259
|
const qs = params.toString();
|
|
7206
7260
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/preview${qs ? `?${qs}` : ""}`);
|
|
7207
7261
|
}
|
|
7208
|
-
/**
|
|
7209
|
-
*
|
|
7210
|
-
*
|
|
7211
|
-
|
|
7262
|
+
/**
|
|
7263
|
+
* Choose which sale route this channel opens.
|
|
7264
|
+
*
|
|
7265
|
+
* Since the server's 2026-08-06 change this is AUTHORIZATION, not a label:
|
|
7266
|
+
* exactly one of the four routes may mint buyer access for the channel and the
|
|
7267
|
+
* other three refuse with 409 `channel_access_intent_forbids`. The default is
|
|
7268
|
+
* `none`, which refuses all four — so a route has to be declared before any
|
|
7269
|
+
* buyer-facing action on the channel can succeed.
|
|
7270
|
+
*
|
|
7271
|
+
* Switching the route while buyers are already inside the current one is
|
|
7272
|
+
* refused with 409 `channel_intent_switch_blocked`, whose `details` name what
|
|
7273
|
+
* is live (`liveAccessLinks`, `activeSessions`). Retry with
|
|
7274
|
+
* `acknowledgeLiveAccess: true`: hosted links on the channel are revoked,
|
|
7275
|
+
* while sessions already minted keep their holds and drain on their own.
|
|
7276
|
+
* `intentSwitch` is present on the response ONLY when the switch disturbed
|
|
7277
|
+
* something, so the ordinary case stays the two-key body it has always been.
|
|
7278
|
+
*/
|
|
7279
|
+
setChannelAccessIntent(key, channelId, accessIntent, opts = {}) {
|
|
7212
7280
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}`, {
|
|
7213
7281
|
method: "PATCH",
|
|
7214
|
-
body: {
|
|
7282
|
+
body: {
|
|
7283
|
+
accessIntent,
|
|
7284
|
+
...opts.acknowledgeLiveAccess ? { acknowledgeLiveAccess: true } : {},
|
|
7285
|
+
...opts.reason ? { reason: opts.reason } : {}
|
|
7286
|
+
}
|
|
7215
7287
|
});
|
|
7216
7288
|
}
|
|
7217
7289
|
// ---- hosted access links (M8) ----
|
|
@@ -7226,8 +7298,11 @@ var ManageApi = class {
|
|
|
7226
7298
|
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
7227
7299
|
* spelled out in `ManageApiError.serverMessage`.
|
|
7228
7300
|
*
|
|
7229
|
-
*
|
|
7230
|
-
* `hosted_link
|
|
7301
|
+
* NOT a side effect any more. This used to SET the channel's access intent to
|
|
7302
|
+
* `hosted_link`; since 2026-08-06 it REQUIRES it, and a channel declaring any
|
|
7303
|
+
* other route refuses with 409 `channel_access_intent_forbids`. Callers must
|
|
7304
|
+
* declare the route first — `ChannelsMode` does exactly that before it
|
|
7305
|
+
* creates, so a first buyer link on a fresh channel is still one gesture.
|
|
7231
7306
|
*/
|
|
7232
7307
|
createAccessLink(key, channelId, input = {}) {
|
|
7233
7308
|
return this.auth(
|
|
@@ -7410,10 +7485,14 @@ var CHANNELS_CSS = (
|
|
|
7410
7485
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7411
7486
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
7412
7487
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
7413
|
-
/* distribute
|
|
7488
|
+
/* distribute routes \u2014 four choices, each with the one sentence that explains it.
|
|
7489
|
+
The current one is NAMED in its own card, never signalled by colour alone. */
|
|
7414
7490
|
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
7415
7491
|
border-radius:10px;background:var(--slm-surface)}
|
|
7416
|
-
.slm-ch-dist
|
|
7492
|
+
.slm-ch-dist.on{border-color:var(--slm-accent,#8b7cf6)}
|
|
7493
|
+
.slm-ch-dist b{font-size:13px;font-weight:800;display:flex;align-items:center;gap:8px;justify-content:space-between}
|
|
7494
|
+
.slm-ch-dist b .cur{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
7495
|
+
color:var(--slm-accent,#8b7cf6);white-space:nowrap}
|
|
7417
7496
|
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7418
7497
|
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
7419
7498
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
@@ -8488,49 +8567,52 @@ var ChannelsMode = class {
|
|
|
8488
8567
|
${lifecycle}`;
|
|
8489
8568
|
}
|
|
8490
8569
|
/**
|
|
8491
|
-
* "Distribute" —
|
|
8570
|
+
* "Distribute" — the ONE way this channel's seats reach a buyer.
|
|
8571
|
+
*
|
|
8572
|
+
* All four routes are here, and this is a real chooser again. It was cut down
|
|
8573
|
+
* to two actions in 0.42.0 for a good reason: `access_intent` was stored,
|
|
8574
|
+
* audited, and read by nothing, so "Keep as protected reserve" and "Sell
|
|
8575
|
+
* through your own staff" were labels an organizer could set and then wait
|
|
8576
|
+
* forever for something to happen. The server closed that hole on 2026-08-06 —
|
|
8577
|
+
* each declaration now opens exactly one route and REFUSES the other three —
|
|
8578
|
+
* so all four are honest choices and belong on the surface.
|
|
8492
8579
|
*
|
|
8493
|
-
*
|
|
8494
|
-
*
|
|
8495
|
-
*
|
|
8496
|
-
* organizer could set and then wait forever for something to happen. A third,
|
|
8497
|
-
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8498
|
-
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8580
|
+
* None of the old copy came back with them. These sentences are written
|
|
8581
|
+
* against the enforcement matrix (`accessIntentDescription`), which is why
|
|
8582
|
+
* each one says what the route refuses as well as what it allows.
|
|
8499
8583
|
*
|
|
8500
|
-
*
|
|
8501
|
-
*
|
|
8502
|
-
*
|
|
8503
|
-
*
|
|
8504
|
-
* value is left alone (it is organizer-declared metadata and the API that
|
|
8505
|
-
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8584
|
+
* The current route is stated, not merely styled: a chooser whose selection
|
|
8585
|
+
* you have to infer from a border is not a chooser. Its card carries a
|
|
8586
|
+
* "Current route" marker and drops its own select button, because pressing it
|
|
8587
|
+
* would do nothing.
|
|
8506
8588
|
*/
|
|
8507
8589
|
distributeHtml(channel) {
|
|
8508
8590
|
const intent = channel.access?.intent ?? "none";
|
|
8509
8591
|
const live = channel.access?.hasActiveGrants === true;
|
|
8510
|
-
const state = live ? intent === "server" ? "Your website is letting buyers in. Only they can buy these seats." : "A buyer link is live. Only people with that link can buy these seats." : intent === "server" ? "Set up for your website \u2014 no buyer has come through yet. Seats stay reserved." : "Not distributed yet \u2014 seats stay reserved.";
|
|
8511
|
-
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8512
|
-
<b>${esc(title)}</b>
|
|
8513
|
-
<span class="why">${esc(why)}</span>
|
|
8514
|
-
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8515
|
-
</div>`;
|
|
8516
8592
|
const linksSupported = this.linksState !== "unsupported";
|
|
8593
|
+
const hasLiveLink = this.links.some(accessLinkIsLive);
|
|
8594
|
+
const state = intent === "none" ? "Protected reserve \u2014 no route can sell these seats. Every buyer path is refused." : intent === "internal" ? "Your staff sell these seats. No buyer-facing route is open." : live ? intent === "server" ? "Your website is letting buyers in. Only they can buy these seats." : "A buyer link is live. Only people with that link can buy these seats." : intent === "server" ? "Set up for your website \u2014 no buyer has come through yet. Seats stay reserved." : "Set up for buyer links \u2014 no buyer has come through yet. Seats stay reserved.";
|
|
8595
|
+
const option = (route, action, cta, primary) => {
|
|
8596
|
+
const current = route === intent;
|
|
8597
|
+
return `<div class="slm-ch-dist${current ? " on" : ""}" data-ch-route="${esc(route)}">
|
|
8598
|
+
<b>${esc(accessIntentLabel(route))}${current ? '<span class="cur">Current route</span>' : ""}</b>
|
|
8599
|
+
<span class="why">${esc(accessIntentDescription(route))}</span>
|
|
8600
|
+
${current && !cta ? "" : `<button type="button" class="slm-btn${primary && !current ? "" : " ghost"}"
|
|
8601
|
+
data-ch-act="${esc(action)}">${esc(cta)}</button>`}
|
|
8602
|
+
</div>`;
|
|
8603
|
+
};
|
|
8517
8604
|
return `
|
|
8518
8605
|
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8519
8606
|
<p class="slm-hint">${esc(state)}</p>
|
|
8520
8607
|
${linksSupported ? option(
|
|
8521
|
-
"
|
|
8522
|
-
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8608
|
+
"hosted_link",
|
|
8523
8609
|
"link-create",
|
|
8524
|
-
|
|
8610
|
+
hasLiveLink ? "Create another buyer link" : "Create buyer link",
|
|
8525
8611
|
true
|
|
8526
8612
|
) : ""}
|
|
8527
|
-
${option(
|
|
8528
|
-
"
|
|
8529
|
-
"
|
|
8530
|
-
"embed-code",
|
|
8531
|
-
"Get embed code",
|
|
8532
|
-
false
|
|
8533
|
-
)}
|
|
8613
|
+
${option("server", "embed-code", intent === "server" ? "Get embed code" : "Use a website or app", false)}
|
|
8614
|
+
${option("internal", "route-internal", intent === "internal" ? "" : "Hand to your staff", false)}
|
|
8615
|
+
${option("none", "route-none", intent === "none" ? "" : "Keep as reserve", false)}
|
|
8534
8616
|
${this.hostedLinksHtml()}
|
|
8535
8617
|
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8536
8618
|
}
|
|
@@ -8778,16 +8860,25 @@ var ChannelsMode = class {
|
|
|
8778
8860
|
this.linksState = "idle";
|
|
8779
8861
|
this.paintRail();
|
|
8780
8862
|
break;
|
|
8863
|
+
// Choosing the buyer-link route IS creating the first link — the dialog
|
|
8864
|
+
// declares the route on submit (see `createLink`), so this stays one
|
|
8865
|
+
// gesture whether the channel is already on `hosted_link` or not.
|
|
8781
8866
|
case "link-create":
|
|
8782
8867
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8783
8868
|
break;
|
|
8784
8869
|
// The one thing this screen can actually do for a website integration is
|
|
8785
|
-
// record that the channel is meant for one —
|
|
8786
|
-
//
|
|
8787
|
-
//
|
|
8870
|
+
// record that the channel is meant for one — and since 2026-08-06 that
|
|
8871
|
+
// record is what AUTHORIZES the integration to mint buyer sessions at all,
|
|
8872
|
+
// so it is the substantive half of the job, not a flag.
|
|
8788
8873
|
case "embed-code":
|
|
8789
8874
|
void this.chooseWebsiteIntegration();
|
|
8790
8875
|
break;
|
|
8876
|
+
case "route-internal":
|
|
8877
|
+
void this.setAccessIntent("internal");
|
|
8878
|
+
break;
|
|
8879
|
+
case "route-none":
|
|
8880
|
+
void this.setAccessIntent("none");
|
|
8881
|
+
break;
|
|
8791
8882
|
case "link-reload":
|
|
8792
8883
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8793
8884
|
break;
|
|
@@ -9094,6 +9185,77 @@ var ChannelsMode = class {
|
|
|
9094
9185
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
9095
9186
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
9096
9187
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
9188
|
+
else if (state.kind === "intentSwitch") this.renderIntentSwitchDialog(state);
|
|
9189
|
+
}
|
|
9190
|
+
/**
|
|
9191
|
+
* `channel_intent_switch_blocked` — buyers are inside the route being left.
|
|
9192
|
+
*
|
|
9193
|
+
* The same review-then-acknowledge shape as the archive and chart-drop guards,
|
|
9194
|
+
* because it is the same kind of decision: the server refuses once, names
|
|
9195
|
+
* exactly what is at stake, and only a deliberate second press goes through.
|
|
9196
|
+
* What acknowledging does is spelled out per consequence — links close now,
|
|
9197
|
+
* checkouts already running survive and drain — rather than hidden behind a
|
|
9198
|
+
* word like "force".
|
|
9199
|
+
*/
|
|
9200
|
+
renderIntentSwitchDialog(state) {
|
|
9201
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9202
|
+
const to = state.intentTo;
|
|
9203
|
+
if (!channel || !to || !this.caps.manage) {
|
|
9204
|
+
this.closeDialog();
|
|
9205
|
+
return;
|
|
9206
|
+
}
|
|
9207
|
+
const { headline, consequences } = intentSwitchBlockedCopy(state.switchBlocked);
|
|
9208
|
+
this.renderScrim(`
|
|
9209
|
+
<h3 id="slm-ch-dlg-title">Change how ${esc(channel.name)} reaches buyers?</h3>
|
|
9210
|
+
<p class="sub">${esc(headline)}</p>
|
|
9211
|
+
<div class="slm-ch-alert warn" role="alert"><span>\u26A0</span><span>
|
|
9212
|
+
${consequences.map((line) => `<span style="display:block;margin-bottom:6px">${esc(line)}</span>`).join("")}
|
|
9213
|
+
</span></div>
|
|
9214
|
+
${state.pendingLink ? `<p class="slm-note">Your new buyer link is created as soon as
|
|
9215
|
+
the route changes \u2014 you will not have to fill the form in again.</p>` : ""}
|
|
9216
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
9217
|
+
<div class="foot">
|
|
9218
|
+
<button type="button" class="quiet" data-ch-close>Leave it as it is</button>
|
|
9219
|
+
<button type="button" class="slm-btn" data-ch-intent-ack${state.busy ? " disabled" : ""}>
|
|
9220
|
+
${state.busy ? "Changing\u2026" : `Change to "${esc(accessIntentLabel(to))}"`}</button>
|
|
9221
|
+
</div>`, (dialog) => {
|
|
9222
|
+
dialog.querySelector("[data-ch-intent-ack]")?.addEventListener("click", () => {
|
|
9223
|
+
void this.acknowledgeIntentSwitch(to, state.pendingLink ?? null);
|
|
9224
|
+
});
|
|
9225
|
+
});
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* The acknowledged retry. The declare and the create stay one gesture across
|
|
9229
|
+
* the sheet: if this switch was the first half of a declare-then-create, the
|
|
9230
|
+
* held form finishes on the far side of the acknowledgement.
|
|
9231
|
+
*/
|
|
9232
|
+
async acknowledgeIntentSwitch(intent, pendingLink) {
|
|
9233
|
+
const channelId = this.detailChannelId;
|
|
9234
|
+
if (!channelId) {
|
|
9235
|
+
this.closeDialog();
|
|
9236
|
+
return;
|
|
9237
|
+
}
|
|
9238
|
+
if (this.dialog) {
|
|
9239
|
+
this.dialog.busy = true;
|
|
9240
|
+
this.renderDialog();
|
|
9241
|
+
}
|
|
9242
|
+
const ok = await this.setAccessIntent(intent, { acknowledgeLiveAccess: true });
|
|
9243
|
+
if (!ok) {
|
|
9244
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
9245
|
+
this.dialog.busy = false;
|
|
9246
|
+
this.renderDialog();
|
|
9247
|
+
}
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
if (!pendingLink) {
|
|
9251
|
+
this.closeDialog();
|
|
9252
|
+
return;
|
|
9253
|
+
}
|
|
9254
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
9255
|
+
this.dialog.busy = false;
|
|
9256
|
+
this.renderDialog();
|
|
9257
|
+
}
|
|
9258
|
+
await this.mintLink(channelId, pendingLink);
|
|
9097
9259
|
}
|
|
9098
9260
|
/**
|
|
9099
9261
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -9434,31 +9596,69 @@ var ChannelsMode = class {
|
|
|
9434
9596
|
});
|
|
9435
9597
|
}
|
|
9436
9598
|
/**
|
|
9437
|
-
* "
|
|
9438
|
-
*
|
|
9439
|
-
*
|
|
9440
|
-
*
|
|
9441
|
-
*
|
|
9599
|
+
* "Use a website or app" — declare the channel's route as `server`.
|
|
9600
|
+
*
|
|
9601
|
+
* This is no longer a flag the Embed page happens to read: since 2026-08-06 it
|
|
9602
|
+
* is what AUTHORIZES `POST /v1/events/:key/buyer-access-sessions` to mint for
|
|
9603
|
+
* this channel at all. Without it, an integration that is otherwise perfectly
|
|
9604
|
+
* wired up gets a 409 on every buyer.
|
|
9442
9605
|
*/
|
|
9443
9606
|
async chooseWebsiteIntegration() {
|
|
9444
9607
|
const channelId = this.detailChannelId;
|
|
9445
9608
|
if (!channelId) return;
|
|
9446
9609
|
await this.setAccessIntent("server");
|
|
9447
9610
|
}
|
|
9448
|
-
|
|
9611
|
+
/**
|
|
9612
|
+
* Declare this channel's sale route.
|
|
9613
|
+
*
|
|
9614
|
+
* Reports through the toast lane the rest of the rail's direct actions use.
|
|
9615
|
+
* The two enforcement refusals get real answers rather than a generic failure:
|
|
9616
|
+
* `channel_intent_switch_blocked` opens the review sheet (there is a decision
|
|
9617
|
+
* to make, and a sheet is where decisions live), and
|
|
9618
|
+
* `channel_access_intent_forbids` — which the organizer can hit by racing
|
|
9619
|
+
* their own second tab — says which route is in the way.
|
|
9620
|
+
*/
|
|
9621
|
+
async setAccessIntent(accessIntent, opts = {}) {
|
|
9449
9622
|
const channelId = this.detailChannelId;
|
|
9450
|
-
if (!channelId || !this.caps.manage) return;
|
|
9623
|
+
if (!channelId || !this.caps.manage) return false;
|
|
9451
9624
|
try {
|
|
9452
|
-
await this.host.api.setChannelAccessIntent(
|
|
9625
|
+
const result = await this.host.api.setChannelAccessIntent(
|
|
9626
|
+
this.host.eventKey,
|
|
9627
|
+
channelId,
|
|
9628
|
+
accessIntent,
|
|
9629
|
+
opts
|
|
9630
|
+
);
|
|
9453
9631
|
await this.refresh();
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
}
|
|
9632
|
+
this.host.toast(this.intentSavedCopy(accessIntent, result?.intentSwitch), "ok");
|
|
9633
|
+
return true;
|
|
9457
9634
|
} catch (err) {
|
|
9458
|
-
|
|
9635
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9636
|
+
this.openDialog({
|
|
9637
|
+
kind: "intentSwitch",
|
|
9638
|
+
channelId,
|
|
9639
|
+
intentTo: accessIntent,
|
|
9640
|
+
switchBlocked: err.details
|
|
9641
|
+
});
|
|
9642
|
+
return false;
|
|
9643
|
+
}
|
|
9644
|
+
if (err instanceof ManageApiError && err.code === "channel_access_intent_forbids") {
|
|
9645
|
+
this.host.toast(intentForbidsCopy(err.details), "err");
|
|
9646
|
+
return false;
|
|
9647
|
+
}
|
|
9648
|
+
this.host.toast("Couldn't change how this channel reaches buyers.", "err");
|
|
9459
9649
|
this.host.onError(err);
|
|
9650
|
+
return false;
|
|
9460
9651
|
}
|
|
9461
9652
|
}
|
|
9653
|
+
/** What just happened, including anything the switch took down with it — the
|
|
9654
|
+
* server reports `intentSwitch` only when it actually disturbed something. */
|
|
9655
|
+
intentSavedCopy(intent, disturbed) {
|
|
9656
|
+
const head = intent === "server" ? "Set to your website or app. The embed code is on the Embed page." : intent === "internal" ? "Only your own staff can sell this channel now." : intent === "hosted_link" ? "Set to buyer links. Create one to let buyers in." : "Kept as a protected reserve. No route can sell these seats.";
|
|
9657
|
+
if (!disturbed) return head;
|
|
9658
|
+
const closed = disturbed.closedLinks ? ` ${disturbed.closedLinks.toLocaleString()} buyer link${disturbed.closedLinks === 1 ? "" : "s"} closed.` : "";
|
|
9659
|
+
const kept = disturbed.keptSessions ? ` ${disturbed.keptSessions.toLocaleString()} buyer${disturbed.keptSessions === 1 ? "" : "s"} already in a checkout can still finish.` : "";
|
|
9660
|
+
return `${head}${closed}${kept}`;
|
|
9661
|
+
}
|
|
9462
9662
|
/** `channelId` is explicit because this is reachable from the ⋯ menu on a row
|
|
9463
9663
|
* that is NOT the open channel, as well as from the detail panel itself. */
|
|
9464
9664
|
async togglePause(channelId = this.detailChannelId) {
|
|
@@ -9711,7 +9911,35 @@ var ChannelsMode = class {
|
|
|
9711
9911
|
});
|
|
9712
9912
|
});
|
|
9713
9913
|
}
|
|
9914
|
+
/**
|
|
9915
|
+
* DECLARE, then create.
|
|
9916
|
+
*
|
|
9917
|
+
* `createAccessLink` used to set the channel's route to `hosted_link` as a
|
|
9918
|
+
* side effect, which is exactly why the picker could never refuse anything.
|
|
9919
|
+
* The server took that side effect away and now REQUIRES the declaration —
|
|
9920
|
+
* and channels default to `none`, so a create that did not declare first
|
|
9921
|
+
* would 409 on the organizer's very first "Create buyer link".
|
|
9922
|
+
*
|
|
9923
|
+
* So the route is declared here, immediately before the create. It stays ONE
|
|
9924
|
+
* gesture: nothing is declared while the organizer is still filling the form
|
|
9925
|
+
* in (cancelling changes nothing), and if the declaration is the part that is
|
|
9926
|
+
* refused, the review sheet holds this form and finishes the job on the far
|
|
9927
|
+
* side of the acknowledgement.
|
|
9928
|
+
*/
|
|
9714
9929
|
async createLink(channelId, input) {
|
|
9930
|
+
if (!await this.ensureHostedLinkRoute(channelId, input)) return;
|
|
9931
|
+
await this.mintLink(channelId, input);
|
|
9932
|
+
}
|
|
9933
|
+
/**
|
|
9934
|
+
* The create half, on its own.
|
|
9935
|
+
*
|
|
9936
|
+
* Separate from `createLink` because the acknowledge path has ALREADY declared
|
|
9937
|
+
* the route — with the very acknowledgement the plain declaration was refused
|
|
9938
|
+
* for. Sending it back through `ensureHostedLinkRoute` would re-derive the
|
|
9939
|
+
* route from a channel list that has not necessarily caught up, and could
|
|
9940
|
+
* refuse the organizer a second time for a decision they just made.
|
|
9941
|
+
*/
|
|
9942
|
+
async mintLink(channelId, input) {
|
|
9715
9943
|
try {
|
|
9716
9944
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
9717
9945
|
this.revealLink(reveal, { channelId });
|
|
@@ -9721,6 +9949,37 @@ var ChannelsMode = class {
|
|
|
9721
9949
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9722
9950
|
}
|
|
9723
9951
|
}
|
|
9952
|
+
/**
|
|
9953
|
+
* Make sure the channel declares the buyer-link route before a link is minted.
|
|
9954
|
+
*
|
|
9955
|
+
* Returns false when the create must NOT proceed — either it was refused, or
|
|
9956
|
+
* the decision has been handed to the switch-review sheet, which resumes it.
|
|
9957
|
+
* A channel already on `hosted_link` costs no request at all, so creating a
|
|
9958
|
+
* second link is the same single call it has always been.
|
|
9959
|
+
*/
|
|
9960
|
+
async ensureHostedLinkRoute(channelId, pendingLink) {
|
|
9961
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9962
|
+
if ((channel?.access?.intent ?? "none") === "hosted_link") return true;
|
|
9963
|
+
try {
|
|
9964
|
+
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, "hosted_link");
|
|
9965
|
+
await this.refresh();
|
|
9966
|
+
return true;
|
|
9967
|
+
} catch (err) {
|
|
9968
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9969
|
+
this.openDialog({
|
|
9970
|
+
kind: "intentSwitch",
|
|
9971
|
+
channelId,
|
|
9972
|
+
intentTo: "hosted_link",
|
|
9973
|
+
switchBlocked: err.details,
|
|
9974
|
+
pendingLink
|
|
9975
|
+
});
|
|
9976
|
+
return false;
|
|
9977
|
+
}
|
|
9978
|
+
this.showDialogError(err instanceof ManageApiError && err.code === "channel_access_intent_forbids" ? intentForbidsCopy(err.details) : "Couldn't set this channel up for buyer links. Try again.");
|
|
9979
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9980
|
+
return false;
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9724
9983
|
/**
|
|
9725
9984
|
* The ONE-TIME reveal.
|
|
9726
9985
|
*
|
|
@@ -12387,6 +12646,7 @@ var SeatManager = class {
|
|
|
12387
12646
|
SeatManager,
|
|
12388
12647
|
SeatPicker,
|
|
12389
12648
|
SeatingChart,
|
|
12649
|
+
accessIntentDescription,
|
|
12390
12650
|
accessIntentLabel,
|
|
12391
12651
|
accessLine,
|
|
12392
12652
|
accessLinkBadge,
|
|
@@ -12399,6 +12659,8 @@ var SeatManager = class {
|
|
|
12399
12659
|
createBuyerAccessContext,
|
|
12400
12660
|
createControllerSink,
|
|
12401
12661
|
dropReviewRows,
|
|
12662
|
+
intentForbidsCopy,
|
|
12663
|
+
intentSwitchBlockedCopy,
|
|
12402
12664
|
isPublicChannelId,
|
|
12403
12665
|
markerLetter,
|
|
12404
12666
|
markerOf,
|