sirius/js/profile-menu.js
Local Dev 56e25d7938 sirius.x: fix electrum listunspent + profile dropdown across all pages
registrar.js: BNS.connect() now filters silentmode.st/electrum and
coinspectrum.duckdns.org:50011 out of the default electrum list. Those
endpoints are our own bns-indexer.js — beacon-only, serves get_history +
transaction.get but NOT listunspent for arbitrary scripthashes. Every
wallet op (getBalance, getUtxos, edit signing) needs listunspent, so
picking a beacon indexer first (which we did for best reachability) broke
every wallet unlock with "-32601 unsupported method: blockchain.
scripthash.listunspent". New `beaconOk: true` opts back in for pure-
resolution paths.

Rebuilt the browser bundle (site/js/bns-register.js) so the fix reaches
portal + admin + register.html + anything else that imports BNS.connect.

site-sirius-x/js/profile-menu.js: a small shared script that transforms
the "🔑 Sign in" nav pill into a dropdown menu on every sirius.x page.
Signed-out shows New wallet / Add wallet / WizardConnect + a link to the
sign-in page. Signed-in (reads localStorage 'siriusProfile') shows the
short address, My names, Admin, Sign out. Included via one <script defer>
tag on each of the 6 pages (landing / portal / admin / docs / theseus /
brand); dropdown CSS is inlined by the script itself so consumers don't
need a matching stylesheet.

Portal.html writing to localStorage.'siriusProfile' after sign-in is a
follow-up so the dropdown reflects state across pages — until then the
menu always shows the onboarding options.
2026-09-06 18:53:17 +02:00

137 lines
6 KiB
JavaScript

// Transforms the "🔑 Sign in" nav pill on every sirius.x page into a dropdown
// exposing the three wallet-onboarding options directly (New / Import /
// WizardConnect). Signed-in state is read from localStorage.'siriusProfile'
// (portal.html writes/clears it on sign-in / sign-out) — when present, the
// menu switches to "profile" mode: address + sign-out link.
//
// One include line per page: <script defer src="/sirius-x/js/profile-menu.js"></script>
// (path is absolute to silentmode.st root; the BCNR gateway serves the same URL).
(() => {
const NS = "silentmode.st";
const PORTAL_URL = "/sirius-x/portal.html";
// Find the .portal anchor already in the nav (the "Sign in" pill).
const anchor = document.querySelector(".topnav a.portal");
if (!anchor) return; // page has no nav-portal button; nothing to do
// Read profile state — portal.html sets this after successful sign-in with
// { address, tokenAddress, source: "seed" | "wc" }; clears on sign-out.
const readProfile = () => {
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
};
const profile = readProfile();
// Build wrapper + dropdown around the existing pill.
const wrap = document.createElement("div");
wrap.className = "profile-wrap";
const btn = document.createElement("button");
btn.type = "button";
btn.className = anchor.className; // reuse .portal (and .here if present)
btn.setAttribute("aria-haspopup", "menu");
btn.setAttribute("aria-expanded", "false");
btn.textContent = profile ? "🔑 " + shortAddr(profile.address) + " ▾" : "🔑 Wallet ▾";
const menu = document.createElement("div");
menu.className = "profile-menu";
menu.setAttribute("role", "menu");
menu.hidden = true;
if (profile) {
menu.innerHTML = `
<div class="profile-header">
<span class="profile-hint">Signed in</span>
<code>${escapeAttr(profile.address)}</code>
</div>
<a href="${PORTAL_URL}" role="menuitem">📋 My names</a>
<a href="/sirius-x/admin/" role="menuitem">🛠 Admin</a>
<hr>
<a href="#" role="menuitem" data-action="signout">🚪 Sign out</a>
`;
} else {
menu.innerHTML = `
<div class="profile-header"><span class="profile-hint">Wallet onboarding</span></div>
<a href="${PORTAL_URL}?mode=new" role="menuitem">🆕 New wallet <span class="mi-hint">generate a fresh seed</span></a>
<a href="${PORTAL_URL}?mode=import" role="menuitem">📥 Add wallet <span class="mi-hint">import a seed phrase</span></a>
<a href="${PORTAL_URL}?mode=wc" role="menuitem">🔗 WizardConnect <span class="mi-hint">Cashonize / Paytaca</span></a>
<hr>
<a href="${PORTAL_URL}" role="menuitem">🔑 Open sign-in page →</a>
`;
}
// Toggle + close-on-outside.
const open = () => { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); };
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
document.addEventListener("click", (e) => { if (!wrap.contains(e.target)) close(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
// Sign-out clears localStorage and reloads (portal.html re-reads state).
menu.addEventListener("click", (e) => {
const t = e.target.closest("[data-action]");
if (t?.dataset.action === "signout") {
e.preventDefault();
try { localStorage.removeItem("siriusProfile"); } catch {}
// Also reload the current page so any signed-in UI resets.
location.reload();
}
});
wrap.appendChild(btn);
wrap.appendChild(menu);
anchor.replaceWith(wrap);
// Inject the dropdown's CSS once. Kept inline so a single script tag is
// the whole install; consumers don't need a matching stylesheet.
if (!document.getElementById("profile-menu-css")) {
const s = document.createElement("style");
s.id = "profile-menu-css";
s.textContent = `
.topnav .profile-wrap { position: relative; margin-left: auto; }
.topnav .profile-wrap button.portal { cursor: pointer; font-family: inherit; }
.topnav .profile-menu {
position: absolute; right: 0; top: calc(100% + 6px);
min-width: 240px; z-index: 30;
background: rgba(20, 26, 36, .98); backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,.12); border-radius: 12px;
padding: 8px; display: flex; flex-direction: column; gap: 2px;
box-shadow: 0 8px 32px rgba(0,0,0,.4);
}
.topnav .profile-menu[hidden] { display: none; }
.topnav .profile-menu > a {
display: flex; flex-direction: column; gap: 2px;
padding: 8px 10px; border-radius: 8px;
color: #e7eaf1; text-decoration: none; font-size: 14px;
}
.topnav .profile-menu > a:hover { background: rgba(214,255,61,.10); color: #d6ff3d; }
.topnav .profile-menu .mi-hint {
color: #5e6678; font-size: 11.5px; font-weight: 400; letter-spacing: 0;
}
.topnav .profile-menu hr {
border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 4px 2px;
}
.topnav .profile-menu .profile-header {
padding: 6px 10px 10px; display: flex; flex-direction: column; gap: 4px;
border-bottom: 1px solid rgba(255,255,255,.08); margin-bottom: 4px;
}
.topnav .profile-menu .profile-hint {
color: #8b98a9; font-size: 11px; text-transform: uppercase; letter-spacing: .4px;
}
.topnav .profile-menu code {
font-family: ui-monospace, monospace; font-size: 11.5px;
color: #e7eaf1; word-break: break-all; background: transparent;
}
`;
document.head.appendChild(s);
}
// ---------- helpers ----------
function shortAddr(a) {
if (!a) return "wallet";
const i = a.indexOf(":");
const body = i > 0 ? a.slice(i + 1) : a;
return body.slice(0, 6) + "…" + body.slice(-4);
}
function escapeAttr(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[c]));
}
})();