eleven / messenger Public

web: fix the space menu's Upgrade link doing nothing under popup blockers (#1322)

James Adam 0c3ac67b75495239694799bba5496a6632c05df5 parent 347dfbe
5 files changed, +39 −9
  • changelog/2026-07-23-upgrade-link-fix.md +10 −0
  • home/web/static/home.js +11 −0
  • web/pages/index.html +1 −1
  • web/static/shell.js +16 −8
  • web/static/style.css +1 −0
diff --git a/changelog/2026-07-23-upgrade-link-fix.md b/changelog/2026-07-23-upgrade-link-fix.md
new file mode 100644
index 0000000..b27fd46
--- /dev/null
+++ b/changelog/2026-07-23-upgrade-link-fix.md
@@ -0,0 +1,10 @@
+🤖 Fixed: the space menu's "Upgrade" item could silently do nothing. It opened
+the home page's checkout in a new tab via a script call, which some browsers'
+ad/privacy-blocker extensions treat as a popup and quietly swallow — no error,
+no new tab, no indication anything was blocked. It's now a real link, the same
+kind any ordinary "opens in a new tab" link uses, so it isn't caught by that
+blocking behavior.
+
+Also: if a space's region has no upgrade plans to offer, the picker used to
+open with a silent blank gap where the plan buttons belong. It now says so
+plainly instead.
diff --git a/home/web/static/home.js b/home/web/static/home.js
index d14d959..6c42a1b 100644
--- a/home/web/static/home.js
+++ b/home/web/static/home.js
@@ -3026,6 +3026,17 @@ function renderUpgrade(s) {
const rates = fxSnapshot && fxSnapshot.rates;
const box = $("upgrade-tiers");
box.textContent = "";
+ // The listing said this space isn't paid, but its region advertised no
+ // tiers to sell (billing not configured there, or momentarily unreachable)
+ // — say so plainly rather than leaving a silent gap where the plan
+ // buttons belong (openUpgrade only gets here once ownership + free-tier
+ // are already confirmed, so the sheet itself is warranted).
+ if (!billingTiers.length) {
+ upgradeStatus("Upgrading isn't available for this space right now.", "error");
+ $("upgrade-fx").classList.add("hidden");
+ $("upgrade-cap").textContent = "";
+ return;
+ }
let anyApprox = false;
for (const t of billingTiers) {
const view = tierForBand(t, selectedBand);
diff --git a/web/pages/index.html b/web/pages/index.html
index bdb0d3d..7bd9682 100644
--- a/web/pages/index.html
+++ b/web/pages/index.html
@@ -157,7 +157,7 @@
platform with a scheme receiver today; iOS follows. -->
<button class="menu-item" id="menu-open-app" role="menuitem" hidden>Open in the Eleven app</button>
<button class="menu-item" id="menu-rename" role="menuitem">Edit profile</button>
- <button class="menu-item" id="menu-upgrade" role="menuitem" hidden>Upgrade</button>
+ <a class="menu-item" id="menu-upgrade" role="menuitem" hidden target="_blank" rel="noopener">Upgrade</a>
<button class="menu-item danger" id="menu-leave" role="menuitem">Leave</button>
</div>
</div>
diff --git a/web/static/shell.js b/web/static/shell.js
index 36e11c4..102f5f4 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -888,12 +888,15 @@ let nativeMenuSeq = 0;
const nativeMenuPending = new Set();
// Pop up `menuEl`'s items as a native menu, anchored under `anchorEl`. Skips
-// items the web layer has hidden (inline display:none — group-only entries, the
-// notifications toggle where push is unusable), so the native menu matches
-// exactly what the HTML one would show.
+// items the web layer has hidden — inline display:none (group-only entries,
+// the notifications toggle where push is unusable) or the `hidden` attribute
+// (Open in the app, Upgrade) — so the native menu matches exactly what the
+// HTML one would show. Missing the `hidden` half of this used to leak
+// Upgrade into the native NSMenu, which the caller deliberately keeps out of
+// native for App Store rules (see the Upgrade wiring below).
function openNativeMenu(menuEl, anchorEl) {
const items = [...menuEl.querySelectorAll(".menu-item")]
- .filter((el) => el.style.display !== "none")
+ .filter((el) => el.style.display !== "none" && !el.hidden)
.map((el) => ({ id: el.id, label: el.textContent.trim(), danger: el.classList.contains("danger") }));
if (!items.length) return;
const token = ++nativeMenuSeq;
@@ -2838,10 +2841,15 @@ function renderApp() {
document.documentElement.classList.contains("native") ||
!!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.lchat);
if (window.__entitlement === "free" && window.__homeOrigin && !inNativeWrapper) {
- document.getElementById("menu-upgrade").hidden = false;
- wireItem("menu-upgrade", () =>
- window.open(window.__homeOrigin + "/#upgrade=" + encodeURIComponent(location.host), "_blank"),
- );
+ // A real <a> (not a button + window.open): ad/privacy-blocker extensions
+ // commonly swallow script-initiated window.open() popups silently — no
+ // console error, no address-bar indicator — while leaving ordinary link
+ // navigation alone. closeMenu() runs as a plain side effect; it never
+ // calls preventDefault, so the browser still follows href natively.
+ const upgradeLink = document.getElementById("menu-upgrade");
+ upgradeLink.hidden = false;
+ upgradeLink.href = window.__homeOrigin + "/#upgrade=" + encodeURIComponent(location.host);
+ upgradeLink.onclick = closeMenu;
}
// Demo countdown band (docs/demo-spaces.md): a demo space wears its 24-hour
diff --git a/web/static/style.css b/web/static/style.css
index aef59d4..493338d 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -479,6 +479,7 @@ select:disabled { opacity: 0.5; cursor: default; }
padding: 10px 12px;
font-weight: 500;
white-space: normal;
+ text-decoration: none; /* menu-upgrade is an <a>; buttons ignore this */
}
.menu-item:hover { background: var(--bubble-them); }
/* Menu rows read danger by text colour alone — the red pill border