@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.js
CHANGED
|
@@ -6493,13 +6493,59 @@ function retryAfterCopy(details) {
|
|
|
6493
6493
|
}
|
|
6494
6494
|
function accessLine(access) {
|
|
6495
6495
|
if (!access || !access.intent) return "\u2014";
|
|
6496
|
-
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "
|
|
6496
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : access.intent === "internal" ? "Your staff sell these" : "Protected reserve";
|
|
6497
6497
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6498
6498
|
const detail = access.detail ?? grants;
|
|
6499
6499
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
6500
6500
|
}
|
|
6501
6501
|
function accessIntentLabel(intent) {
|
|
6502
|
-
return intent === "internal" ? "
|
|
6502
|
+
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";
|
|
6503
|
+
}
|
|
6504
|
+
function accessIntentDescription(intent) {
|
|
6505
|
+
switch (intent) {
|
|
6506
|
+
case "internal":
|
|
6507
|
+
return "Only your own box office can sell these seats, through your secret key. Buyer links and website integrations are refused.";
|
|
6508
|
+
case "server":
|
|
6509
|
+
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.";
|
|
6510
|
+
case "hosted_link":
|
|
6511
|
+
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.";
|
|
6512
|
+
default:
|
|
6513
|
+
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.";
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
function intentForRoute(route) {
|
|
6517
|
+
return route === "hosted_link" ? "hosted_link" : route === "server" ? "server" : route === "staff" ? "internal" : null;
|
|
6518
|
+
}
|
|
6519
|
+
function intentForbidsCopy(details) {
|
|
6520
|
+
const current = parseIntent(details?.accessIntent);
|
|
6521
|
+
const wanted = intentForRoute(details?.route);
|
|
6522
|
+
const head = `This channel is set to "${accessIntentLabel(current)}"`;
|
|
6523
|
+
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.`;
|
|
6524
|
+
}
|
|
6525
|
+
function parseIntent(value) {
|
|
6526
|
+
return value === "internal" || value === "server" || value === "hosted_link" || value === "none" ? value : "none";
|
|
6527
|
+
}
|
|
6528
|
+
function plural(count, one, many) {
|
|
6529
|
+
return `${count.toLocaleString()} ${count === 1 ? one : many}`;
|
|
6530
|
+
}
|
|
6531
|
+
function intentSwitchBlockedCopy(details) {
|
|
6532
|
+
const links = Math.max(0, details?.liveAccessLinks ?? 0);
|
|
6533
|
+
const sessions = Math.max(0, details?.activeSessions ?? 0);
|
|
6534
|
+
const from = parseIntent(details?.from);
|
|
6535
|
+
const to = parseIntent(details?.to);
|
|
6536
|
+
const live = [
|
|
6537
|
+
links ? plural(links, "buyer link is live", "buyer links are live") : null,
|
|
6538
|
+
sessions ? plural(sessions, "buyer is in a checkout", "buyers are in a checkout") : null
|
|
6539
|
+
].filter(Boolean).join(", and ");
|
|
6540
|
+
const headline = `${live || "Buyers are inside this channel"} on "${accessIntentLabel(from)}". Moving it to "${accessIntentLabel(to)}" changes what happens to them.`;
|
|
6541
|
+
const consequences = [];
|
|
6542
|
+
if (links) {
|
|
6543
|
+
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.`);
|
|
6544
|
+
}
|
|
6545
|
+
if (sessions) {
|
|
6546
|
+
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.`);
|
|
6547
|
+
}
|
|
6548
|
+
return { headline, consequences };
|
|
6503
6549
|
}
|
|
6504
6550
|
var ACCESS_LINK_DEFAULTS = {
|
|
6505
6551
|
maxRedemptions: 100,
|
|
@@ -6564,6 +6610,11 @@ function accessLinkErrorCopy(err) {
|
|
|
6564
6610
|
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6565
6611
|
case "channel_unavailable":
|
|
6566
6612
|
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
6613
|
+
// The channel declares a different sale route. The UI declares `hosted_link`
|
|
6614
|
+
// before it creates, so reaching this means the declaration itself was
|
|
6615
|
+
// refused or raced — say which route is in the way, not the code.
|
|
6616
|
+
case "channel_access_intent_forbids":
|
|
6617
|
+
return intentForbidsCopy(err?.details);
|
|
6567
6618
|
case "end_active_sessions_required":
|
|
6568
6619
|
return "Choose what happens to the buyers who already came in through this link.";
|
|
6569
6620
|
case "not_found":
|
|
@@ -6797,13 +6848,31 @@ var ManageApi = class {
|
|
|
6797
6848
|
const qs = params.toString();
|
|
6798
6849
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/preview${qs ? `?${qs}` : ""}`);
|
|
6799
6850
|
}
|
|
6800
|
-
/**
|
|
6801
|
-
*
|
|
6802
|
-
*
|
|
6803
|
-
|
|
6851
|
+
/**
|
|
6852
|
+
* Choose which sale route this channel opens.
|
|
6853
|
+
*
|
|
6854
|
+
* Since the server's 2026-08-06 change this is AUTHORIZATION, not a label:
|
|
6855
|
+
* exactly one of the four routes may mint buyer access for the channel and the
|
|
6856
|
+
* other three refuse with 409 `channel_access_intent_forbids`. The default is
|
|
6857
|
+
* `none`, which refuses all four — so a route has to be declared before any
|
|
6858
|
+
* buyer-facing action on the channel can succeed.
|
|
6859
|
+
*
|
|
6860
|
+
* Switching the route while buyers are already inside the current one is
|
|
6861
|
+
* refused with 409 `channel_intent_switch_blocked`, whose `details` name what
|
|
6862
|
+
* is live (`liveAccessLinks`, `activeSessions`). Retry with
|
|
6863
|
+
* `acknowledgeLiveAccess: true`: hosted links on the channel are revoked,
|
|
6864
|
+
* while sessions already minted keep their holds and drain on their own.
|
|
6865
|
+
* `intentSwitch` is present on the response ONLY when the switch disturbed
|
|
6866
|
+
* something, so the ordinary case stays the two-key body it has always been.
|
|
6867
|
+
*/
|
|
6868
|
+
setChannelAccessIntent(key, channelId, accessIntent, opts = {}) {
|
|
6804
6869
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}`, {
|
|
6805
6870
|
method: "PATCH",
|
|
6806
|
-
body: {
|
|
6871
|
+
body: {
|
|
6872
|
+
accessIntent,
|
|
6873
|
+
...opts.acknowledgeLiveAccess ? { acknowledgeLiveAccess: true } : {},
|
|
6874
|
+
...opts.reason ? { reason: opts.reason } : {}
|
|
6875
|
+
}
|
|
6807
6876
|
});
|
|
6808
6877
|
}
|
|
6809
6878
|
// ---- hosted access links (M8) ----
|
|
@@ -6818,8 +6887,11 @@ var ManageApi = class {
|
|
|
6818
6887
|
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
6819
6888
|
* spelled out in `ManageApiError.serverMessage`.
|
|
6820
6889
|
*
|
|
6821
|
-
*
|
|
6822
|
-
* `hosted_link
|
|
6890
|
+
* NOT a side effect any more. This used to SET the channel's access intent to
|
|
6891
|
+
* `hosted_link`; since 2026-08-06 it REQUIRES it, and a channel declaring any
|
|
6892
|
+
* other route refuses with 409 `channel_access_intent_forbids`. Callers must
|
|
6893
|
+
* declare the route first — `ChannelsMode` does exactly that before it
|
|
6894
|
+
* creates, so a first buyer link on a fresh channel is still one gesture.
|
|
6823
6895
|
*/
|
|
6824
6896
|
createAccessLink(key, channelId, input = {}) {
|
|
6825
6897
|
return this.auth(
|
|
@@ -7002,10 +7074,14 @@ var CHANNELS_CSS = (
|
|
|
7002
7074
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7003
7075
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
7004
7076
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
7005
|
-
/* distribute
|
|
7077
|
+
/* distribute routes \u2014 four choices, each with the one sentence that explains it.
|
|
7078
|
+
The current one is NAMED in its own card, never signalled by colour alone. */
|
|
7006
7079
|
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
7007
7080
|
border-radius:10px;background:var(--slm-surface)}
|
|
7008
|
-
.slm-ch-dist
|
|
7081
|
+
.slm-ch-dist.on{border-color:var(--slm-accent,#8b7cf6)}
|
|
7082
|
+
.slm-ch-dist b{font-size:13px;font-weight:800;display:flex;align-items:center;gap:8px;justify-content:space-between}
|
|
7083
|
+
.slm-ch-dist b .cur{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
7084
|
+
color:var(--slm-accent,#8b7cf6);white-space:nowrap}
|
|
7009
7085
|
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7010
7086
|
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
7011
7087
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
@@ -8080,49 +8156,52 @@ var ChannelsMode = class {
|
|
|
8080
8156
|
${lifecycle}`;
|
|
8081
8157
|
}
|
|
8082
8158
|
/**
|
|
8083
|
-
* "Distribute" —
|
|
8159
|
+
* "Distribute" — the ONE way this channel's seats reach a buyer.
|
|
8160
|
+
*
|
|
8161
|
+
* All four routes are here, and this is a real chooser again. It was cut down
|
|
8162
|
+
* to two actions in 0.42.0 for a good reason: `access_intent` was stored,
|
|
8163
|
+
* audited, and read by nothing, so "Keep as protected reserve" and "Sell
|
|
8164
|
+
* through your own staff" were labels an organizer could set and then wait
|
|
8165
|
+
* forever for something to happen. The server closed that hole on 2026-08-06 —
|
|
8166
|
+
* each declaration now opens exactly one route and REFUSES the other three —
|
|
8167
|
+
* so all four are honest choices and belong on the surface.
|
|
8084
8168
|
*
|
|
8085
|
-
*
|
|
8086
|
-
*
|
|
8087
|
-
*
|
|
8088
|
-
* organizer could set and then wait forever for something to happen. A third,
|
|
8089
|
-
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8090
|
-
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8169
|
+
* None of the old copy came back with them. These sentences are written
|
|
8170
|
+
* against the enforcement matrix (`accessIntentDescription`), which is why
|
|
8171
|
+
* each one says what the route refuses as well as what it allows.
|
|
8091
8172
|
*
|
|
8092
|
-
*
|
|
8093
|
-
*
|
|
8094
|
-
*
|
|
8095
|
-
*
|
|
8096
|
-
* value is left alone (it is organizer-declared metadata and the API that
|
|
8097
|
-
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8173
|
+
* The current route is stated, not merely styled: a chooser whose selection
|
|
8174
|
+
* you have to infer from a border is not a chooser. Its card carries a
|
|
8175
|
+
* "Current route" marker and drops its own select button, because pressing it
|
|
8176
|
+
* would do nothing.
|
|
8098
8177
|
*/
|
|
8099
8178
|
distributeHtml(channel) {
|
|
8100
8179
|
const intent = channel.access?.intent ?? "none";
|
|
8101
8180
|
const live = channel.access?.hasActiveGrants === true;
|
|
8102
|
-
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.";
|
|
8103
|
-
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8104
|
-
<b>${esc(title)}</b>
|
|
8105
|
-
<span class="why">${esc(why)}</span>
|
|
8106
|
-
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8107
|
-
</div>`;
|
|
8108
8181
|
const linksSupported = this.linksState !== "unsupported";
|
|
8182
|
+
const hasLiveLink = this.links.some(accessLinkIsLive);
|
|
8183
|
+
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.";
|
|
8184
|
+
const option = (route, action, cta, primary) => {
|
|
8185
|
+
const current = route === intent;
|
|
8186
|
+
return `<div class="slm-ch-dist${current ? " on" : ""}" data-ch-route="${esc(route)}">
|
|
8187
|
+
<b>${esc(accessIntentLabel(route))}${current ? '<span class="cur">Current route</span>' : ""}</b>
|
|
8188
|
+
<span class="why">${esc(accessIntentDescription(route))}</span>
|
|
8189
|
+
${current && !cta ? "" : `<button type="button" class="slm-btn${primary && !current ? "" : " ghost"}"
|
|
8190
|
+
data-ch-act="${esc(action)}">${esc(cta)}</button>`}
|
|
8191
|
+
</div>`;
|
|
8192
|
+
};
|
|
8109
8193
|
return `
|
|
8110
8194
|
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8111
8195
|
<p class="slm-hint">${esc(state)}</p>
|
|
8112
8196
|
${linksSupported ? option(
|
|
8113
|
-
"
|
|
8114
|
-
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8197
|
+
"hosted_link",
|
|
8115
8198
|
"link-create",
|
|
8116
|
-
|
|
8199
|
+
hasLiveLink ? "Create another buyer link" : "Create buyer link",
|
|
8117
8200
|
true
|
|
8118
8201
|
) : ""}
|
|
8119
|
-
${option(
|
|
8120
|
-
"
|
|
8121
|
-
"
|
|
8122
|
-
"embed-code",
|
|
8123
|
-
"Get embed code",
|
|
8124
|
-
false
|
|
8125
|
-
)}
|
|
8202
|
+
${option("server", "embed-code", intent === "server" ? "Get embed code" : "Use a website or app", false)}
|
|
8203
|
+
${option("internal", "route-internal", intent === "internal" ? "" : "Hand to your staff", false)}
|
|
8204
|
+
${option("none", "route-none", intent === "none" ? "" : "Keep as reserve", false)}
|
|
8126
8205
|
${this.hostedLinksHtml()}
|
|
8127
8206
|
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8128
8207
|
}
|
|
@@ -8370,16 +8449,25 @@ var ChannelsMode = class {
|
|
|
8370
8449
|
this.linksState = "idle";
|
|
8371
8450
|
this.paintRail();
|
|
8372
8451
|
break;
|
|
8452
|
+
// Choosing the buyer-link route IS creating the first link — the dialog
|
|
8453
|
+
// declares the route on submit (see `createLink`), so this stays one
|
|
8454
|
+
// gesture whether the channel is already on `hosted_link` or not.
|
|
8373
8455
|
case "link-create":
|
|
8374
8456
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8375
8457
|
break;
|
|
8376
8458
|
// The one thing this screen can actually do for a website integration is
|
|
8377
|
-
// record that the channel is meant for one —
|
|
8378
|
-
//
|
|
8379
|
-
//
|
|
8459
|
+
// record that the channel is meant for one — and since 2026-08-06 that
|
|
8460
|
+
// record is what AUTHORIZES the integration to mint buyer sessions at all,
|
|
8461
|
+
// so it is the substantive half of the job, not a flag.
|
|
8380
8462
|
case "embed-code":
|
|
8381
8463
|
void this.chooseWebsiteIntegration();
|
|
8382
8464
|
break;
|
|
8465
|
+
case "route-internal":
|
|
8466
|
+
void this.setAccessIntent("internal");
|
|
8467
|
+
break;
|
|
8468
|
+
case "route-none":
|
|
8469
|
+
void this.setAccessIntent("none");
|
|
8470
|
+
break;
|
|
8383
8471
|
case "link-reload":
|
|
8384
8472
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8385
8473
|
break;
|
|
@@ -8686,6 +8774,77 @@ var ChannelsMode = class {
|
|
|
8686
8774
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
8687
8775
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
8688
8776
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
8777
|
+
else if (state.kind === "intentSwitch") this.renderIntentSwitchDialog(state);
|
|
8778
|
+
}
|
|
8779
|
+
/**
|
|
8780
|
+
* `channel_intent_switch_blocked` — buyers are inside the route being left.
|
|
8781
|
+
*
|
|
8782
|
+
* The same review-then-acknowledge shape as the archive and chart-drop guards,
|
|
8783
|
+
* because it is the same kind of decision: the server refuses once, names
|
|
8784
|
+
* exactly what is at stake, and only a deliberate second press goes through.
|
|
8785
|
+
* What acknowledging does is spelled out per consequence — links close now,
|
|
8786
|
+
* checkouts already running survive and drain — rather than hidden behind a
|
|
8787
|
+
* word like "force".
|
|
8788
|
+
*/
|
|
8789
|
+
renderIntentSwitchDialog(state) {
|
|
8790
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8791
|
+
const to = state.intentTo;
|
|
8792
|
+
if (!channel || !to || !this.caps.manage) {
|
|
8793
|
+
this.closeDialog();
|
|
8794
|
+
return;
|
|
8795
|
+
}
|
|
8796
|
+
const { headline, consequences } = intentSwitchBlockedCopy(state.switchBlocked);
|
|
8797
|
+
this.renderScrim(`
|
|
8798
|
+
<h3 id="slm-ch-dlg-title">Change how ${esc(channel.name)} reaches buyers?</h3>
|
|
8799
|
+
<p class="sub">${esc(headline)}</p>
|
|
8800
|
+
<div class="slm-ch-alert warn" role="alert"><span>\u26A0</span><span>
|
|
8801
|
+
${consequences.map((line) => `<span style="display:block;margin-bottom:6px">${esc(line)}</span>`).join("")}
|
|
8802
|
+
</span></div>
|
|
8803
|
+
${state.pendingLink ? `<p class="slm-note">Your new buyer link is created as soon as
|
|
8804
|
+
the route changes \u2014 you will not have to fill the form in again.</p>` : ""}
|
|
8805
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8806
|
+
<div class="foot">
|
|
8807
|
+
<button type="button" class="quiet" data-ch-close>Leave it as it is</button>
|
|
8808
|
+
<button type="button" class="slm-btn" data-ch-intent-ack${state.busy ? " disabled" : ""}>
|
|
8809
|
+
${state.busy ? "Changing\u2026" : `Change to "${esc(accessIntentLabel(to))}"`}</button>
|
|
8810
|
+
</div>`, (dialog) => {
|
|
8811
|
+
dialog.querySelector("[data-ch-intent-ack]")?.addEventListener("click", () => {
|
|
8812
|
+
void this.acknowledgeIntentSwitch(to, state.pendingLink ?? null);
|
|
8813
|
+
});
|
|
8814
|
+
});
|
|
8815
|
+
}
|
|
8816
|
+
/**
|
|
8817
|
+
* The acknowledged retry. The declare and the create stay one gesture across
|
|
8818
|
+
* the sheet: if this switch was the first half of a declare-then-create, the
|
|
8819
|
+
* held form finishes on the far side of the acknowledgement.
|
|
8820
|
+
*/
|
|
8821
|
+
async acknowledgeIntentSwitch(intent, pendingLink) {
|
|
8822
|
+
const channelId = this.detailChannelId;
|
|
8823
|
+
if (!channelId) {
|
|
8824
|
+
this.closeDialog();
|
|
8825
|
+
return;
|
|
8826
|
+
}
|
|
8827
|
+
if (this.dialog) {
|
|
8828
|
+
this.dialog.busy = true;
|
|
8829
|
+
this.renderDialog();
|
|
8830
|
+
}
|
|
8831
|
+
const ok = await this.setAccessIntent(intent, { acknowledgeLiveAccess: true });
|
|
8832
|
+
if (!ok) {
|
|
8833
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
8834
|
+
this.dialog.busy = false;
|
|
8835
|
+
this.renderDialog();
|
|
8836
|
+
}
|
|
8837
|
+
return;
|
|
8838
|
+
}
|
|
8839
|
+
if (!pendingLink) {
|
|
8840
|
+
this.closeDialog();
|
|
8841
|
+
return;
|
|
8842
|
+
}
|
|
8843
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
8844
|
+
this.dialog.busy = false;
|
|
8845
|
+
this.renderDialog();
|
|
8846
|
+
}
|
|
8847
|
+
await this.mintLink(channelId, pendingLink);
|
|
8689
8848
|
}
|
|
8690
8849
|
/**
|
|
8691
8850
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -9026,31 +9185,69 @@ var ChannelsMode = class {
|
|
|
9026
9185
|
});
|
|
9027
9186
|
}
|
|
9028
9187
|
/**
|
|
9029
|
-
* "
|
|
9030
|
-
*
|
|
9031
|
-
*
|
|
9032
|
-
*
|
|
9033
|
-
*
|
|
9188
|
+
* "Use a website or app" — declare the channel's route as `server`.
|
|
9189
|
+
*
|
|
9190
|
+
* This is no longer a flag the Embed page happens to read: since 2026-08-06 it
|
|
9191
|
+
* is what AUTHORIZES `POST /v1/events/:key/buyer-access-sessions` to mint for
|
|
9192
|
+
* this channel at all. Without it, an integration that is otherwise perfectly
|
|
9193
|
+
* wired up gets a 409 on every buyer.
|
|
9034
9194
|
*/
|
|
9035
9195
|
async chooseWebsiteIntegration() {
|
|
9036
9196
|
const channelId = this.detailChannelId;
|
|
9037
9197
|
if (!channelId) return;
|
|
9038
9198
|
await this.setAccessIntent("server");
|
|
9039
9199
|
}
|
|
9040
|
-
|
|
9200
|
+
/**
|
|
9201
|
+
* Declare this channel's sale route.
|
|
9202
|
+
*
|
|
9203
|
+
* Reports through the toast lane the rest of the rail's direct actions use.
|
|
9204
|
+
* The two enforcement refusals get real answers rather than a generic failure:
|
|
9205
|
+
* `channel_intent_switch_blocked` opens the review sheet (there is a decision
|
|
9206
|
+
* to make, and a sheet is where decisions live), and
|
|
9207
|
+
* `channel_access_intent_forbids` — which the organizer can hit by racing
|
|
9208
|
+
* their own second tab — says which route is in the way.
|
|
9209
|
+
*/
|
|
9210
|
+
async setAccessIntent(accessIntent, opts = {}) {
|
|
9041
9211
|
const channelId = this.detailChannelId;
|
|
9042
|
-
if (!channelId || !this.caps.manage) return;
|
|
9212
|
+
if (!channelId || !this.caps.manage) return false;
|
|
9043
9213
|
try {
|
|
9044
|
-
await this.host.api.setChannelAccessIntent(
|
|
9214
|
+
const result = await this.host.api.setChannelAccessIntent(
|
|
9215
|
+
this.host.eventKey,
|
|
9216
|
+
channelId,
|
|
9217
|
+
accessIntent,
|
|
9218
|
+
opts
|
|
9219
|
+
);
|
|
9045
9220
|
await this.refresh();
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
}
|
|
9221
|
+
this.host.toast(this.intentSavedCopy(accessIntent, result?.intentSwitch), "ok");
|
|
9222
|
+
return true;
|
|
9049
9223
|
} catch (err) {
|
|
9050
|
-
|
|
9224
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9225
|
+
this.openDialog({
|
|
9226
|
+
kind: "intentSwitch",
|
|
9227
|
+
channelId,
|
|
9228
|
+
intentTo: accessIntent,
|
|
9229
|
+
switchBlocked: err.details
|
|
9230
|
+
});
|
|
9231
|
+
return false;
|
|
9232
|
+
}
|
|
9233
|
+
if (err instanceof ManageApiError && err.code === "channel_access_intent_forbids") {
|
|
9234
|
+
this.host.toast(intentForbidsCopy(err.details), "err");
|
|
9235
|
+
return false;
|
|
9236
|
+
}
|
|
9237
|
+
this.host.toast("Couldn't change how this channel reaches buyers.", "err");
|
|
9051
9238
|
this.host.onError(err);
|
|
9239
|
+
return false;
|
|
9052
9240
|
}
|
|
9053
9241
|
}
|
|
9242
|
+
/** What just happened, including anything the switch took down with it — the
|
|
9243
|
+
* server reports `intentSwitch` only when it actually disturbed something. */
|
|
9244
|
+
intentSavedCopy(intent, disturbed) {
|
|
9245
|
+
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.";
|
|
9246
|
+
if (!disturbed) return head;
|
|
9247
|
+
const closed = disturbed.closedLinks ? ` ${disturbed.closedLinks.toLocaleString()} buyer link${disturbed.closedLinks === 1 ? "" : "s"} closed.` : "";
|
|
9248
|
+
const kept = disturbed.keptSessions ? ` ${disturbed.keptSessions.toLocaleString()} buyer${disturbed.keptSessions === 1 ? "" : "s"} already in a checkout can still finish.` : "";
|
|
9249
|
+
return `${head}${closed}${kept}`;
|
|
9250
|
+
}
|
|
9054
9251
|
/** `channelId` is explicit because this is reachable from the ⋯ menu on a row
|
|
9055
9252
|
* that is NOT the open channel, as well as from the detail panel itself. */
|
|
9056
9253
|
async togglePause(channelId = this.detailChannelId) {
|
|
@@ -9303,7 +9500,35 @@ var ChannelsMode = class {
|
|
|
9303
9500
|
});
|
|
9304
9501
|
});
|
|
9305
9502
|
}
|
|
9503
|
+
/**
|
|
9504
|
+
* DECLARE, then create.
|
|
9505
|
+
*
|
|
9506
|
+
* `createAccessLink` used to set the channel's route to `hosted_link` as a
|
|
9507
|
+
* side effect, which is exactly why the picker could never refuse anything.
|
|
9508
|
+
* The server took that side effect away and now REQUIRES the declaration —
|
|
9509
|
+
* and channels default to `none`, so a create that did not declare first
|
|
9510
|
+
* would 409 on the organizer's very first "Create buyer link".
|
|
9511
|
+
*
|
|
9512
|
+
* So the route is declared here, immediately before the create. It stays ONE
|
|
9513
|
+
* gesture: nothing is declared while the organizer is still filling the form
|
|
9514
|
+
* in (cancelling changes nothing), and if the declaration is the part that is
|
|
9515
|
+
* refused, the review sheet holds this form and finishes the job on the far
|
|
9516
|
+
* side of the acknowledgement.
|
|
9517
|
+
*/
|
|
9306
9518
|
async createLink(channelId, input) {
|
|
9519
|
+
if (!await this.ensureHostedLinkRoute(channelId, input)) return;
|
|
9520
|
+
await this.mintLink(channelId, input);
|
|
9521
|
+
}
|
|
9522
|
+
/**
|
|
9523
|
+
* The create half, on its own.
|
|
9524
|
+
*
|
|
9525
|
+
* Separate from `createLink` because the acknowledge path has ALREADY declared
|
|
9526
|
+
* the route — with the very acknowledgement the plain declaration was refused
|
|
9527
|
+
* for. Sending it back through `ensureHostedLinkRoute` would re-derive the
|
|
9528
|
+
* route from a channel list that has not necessarily caught up, and could
|
|
9529
|
+
* refuse the organizer a second time for a decision they just made.
|
|
9530
|
+
*/
|
|
9531
|
+
async mintLink(channelId, input) {
|
|
9307
9532
|
try {
|
|
9308
9533
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
9309
9534
|
this.revealLink(reveal, { channelId });
|
|
@@ -9313,6 +9538,37 @@ var ChannelsMode = class {
|
|
|
9313
9538
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9314
9539
|
}
|
|
9315
9540
|
}
|
|
9541
|
+
/**
|
|
9542
|
+
* Make sure the channel declares the buyer-link route before a link is minted.
|
|
9543
|
+
*
|
|
9544
|
+
* Returns false when the create must NOT proceed — either it was refused, or
|
|
9545
|
+
* the decision has been handed to the switch-review sheet, which resumes it.
|
|
9546
|
+
* A channel already on `hosted_link` costs no request at all, so creating a
|
|
9547
|
+
* second link is the same single call it has always been.
|
|
9548
|
+
*/
|
|
9549
|
+
async ensureHostedLinkRoute(channelId, pendingLink) {
|
|
9550
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9551
|
+
if ((channel?.access?.intent ?? "none") === "hosted_link") return true;
|
|
9552
|
+
try {
|
|
9553
|
+
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, "hosted_link");
|
|
9554
|
+
await this.refresh();
|
|
9555
|
+
return true;
|
|
9556
|
+
} catch (err) {
|
|
9557
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9558
|
+
this.openDialog({
|
|
9559
|
+
kind: "intentSwitch",
|
|
9560
|
+
channelId,
|
|
9561
|
+
intentTo: "hosted_link",
|
|
9562
|
+
switchBlocked: err.details,
|
|
9563
|
+
pendingLink
|
|
9564
|
+
});
|
|
9565
|
+
return false;
|
|
9566
|
+
}
|
|
9567
|
+
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.");
|
|
9568
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9569
|
+
return false;
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9316
9572
|
/**
|
|
9317
9573
|
* The ONE-TIME reveal.
|
|
9318
9574
|
*
|
|
@@ -11978,6 +12234,7 @@ export {
|
|
|
11978
12234
|
SeatManager,
|
|
11979
12235
|
SeatPicker,
|
|
11980
12236
|
SeatingChart,
|
|
12237
|
+
accessIntentDescription,
|
|
11981
12238
|
accessIntentLabel,
|
|
11982
12239
|
accessLine,
|
|
11983
12240
|
accessLinkBadge,
|
|
@@ -11990,6 +12247,8 @@ export {
|
|
|
11990
12247
|
createBuyerAccessContext,
|
|
11991
12248
|
createControllerSink,
|
|
11992
12249
|
dropReviewRows,
|
|
12250
|
+
intentForbidsCopy,
|
|
12251
|
+
intentSwitchBlockedCopy,
|
|
11993
12252
|
isPublicChannelId,
|
|
11994
12253
|
markerLetter,
|
|
11995
12254
|
markerOf,
|