Nav wallet dropdown was calling window.siriusSignInWallet(action),
which opens the shared modal on whatever page you were on — so from
tld.html or docs/, a Create/Import/WC click drew the sign-in over the
current page as a full-screen overlay. User: 'still opens as a
separate console on top of the page, it should be within the page'.
Now the click handler:
- if we are already on portal.html AND
siriusRenderSignInInline is loaded, paint the tabbed sign-in
inside portal's #signin-inline directly and scroll to it
- otherwise navigate to portal.html?mode=<action>, where the
bootstrap already renders the tabbed sign-in inline as the
page's own content
Same tabs, same forms — the four options (🔓 Unlock / 🆕 Create /
📥 Import / 🔗 WizardConnect) — but they now live inside the page,
not on top of it. Unlock falls back to Import when there's no saved
wallet in that browser.
267 lines
13 KiB
JavaScript
267 lines
13 KiB
JavaScript
// Transforms the "🔑 Sign in" nav pill on every Sirius.X page into a wallet
|
|
// dropdown. Reads siriusProfile from localStorage; when null shows the three
|
|
// onboarding actions (New wallet / Import a phrase / WizardConnect), when
|
|
// present shows the signed-in menu (address, My names, Admin, Sign out).
|
|
//
|
|
// Each onboarding action deep-links portal.html?mode=new|import|wc; portal.html
|
|
// picks that up and immediately runs the matching flow — no extra click on
|
|
// arrival. Signed-in state changes flip the button label without a reload:
|
|
// the storage event covers cross-tab, a custom 'siriusProfileChanged' event
|
|
// covers same-tab (portal.html and tld.html both dispatch it on write/clear).
|
|
|
|
(() => {
|
|
const anchor = document.querySelector(".topnav a.portal");
|
|
if (!anchor) return;
|
|
|
|
// Base URL for local links — the nav's brand link is already set with the
|
|
// right relative path per page (./ at root, ../ in subdirs). Reuse it so
|
|
// absolute /sirius-x/... paths (which break under the BCNR route) never
|
|
// slip in.
|
|
const PORTAL_URL = anchor.getAttribute("href") || "./portal.html";
|
|
const BASE = PORTAL_URL.replace(/portal\.html.*$/, "");
|
|
const ADMIN_URL = BASE + "admin/";
|
|
|
|
const readProfile = () => {
|
|
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
|
|
};
|
|
|
|
// Build wrapper + button + (empty) menu 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");
|
|
const menu = document.createElement("div");
|
|
menu.className = "profile-menu";
|
|
menu.setAttribute("role", "menu");
|
|
menu.hidden = true;
|
|
|
|
// ---------- render ----------
|
|
// Re-renders the button label AND the menu innerHTML from the current
|
|
// profile in localStorage. Called at load, on every open(), and whenever
|
|
// another tab or this tab updates the storage key.
|
|
function render() {
|
|
const p = readProfile();
|
|
if (p) {
|
|
const dot = `<span class="wallet-dot ok" aria-hidden="true"></span>`;
|
|
const label = shortAddr(p.address);
|
|
const sourceBadge = p.source === "wc"
|
|
? `<span class="wallet-src">WC</span>`
|
|
: `<span class="wallet-src">seed</span>`;
|
|
btn.innerHTML = `${dot}<span class="wallet-label">${escapeAttr(label)}</span>${sourceBadge}<span class="wallet-caret" aria-hidden="true">▾</span>`;
|
|
btn.setAttribute("aria-label", `Wallet ${label}, signed in via ${p.source || "seed"}`);
|
|
menu.innerHTML = `
|
|
<div class="profile-header">
|
|
<span class="profile-hint">Signed in via ${escapeAttr(p.source === "wc" ? "WizardConnect" : "seed phrase")}</span>
|
|
<code>${escapeAttr(p.address)}</code>
|
|
</div>
|
|
<a href="${PORTAL_URL}" role="menuitem">
|
|
<span class="mi-ic">📋</span>
|
|
<span class="mi-body"><b>My names</b><span class="mi-hint">records & holdings</span></span>
|
|
</a>
|
|
<a href="${ADMIN_URL}" role="menuitem">
|
|
<span class="mi-ic">🛠</span>
|
|
<span class="mi-body"><b>Admin panel</b><span class="mi-hint">operator tools</span></span>
|
|
</a>
|
|
<hr>
|
|
<a href="#" role="menuitem" data-action="signout">
|
|
<span class="mi-ic">🚪</span>
|
|
<span class="mi-body"><b>Sign out</b><span class="mi-hint">clear this browser</span></span>
|
|
</a>
|
|
`;
|
|
} else {
|
|
const dot = `<span class="wallet-dot" aria-hidden="true"></span>`;
|
|
btn.innerHTML = `${dot}<span class="wallet-label">Wallet</span><span class="wallet-caret" aria-hidden="true">▾</span>`;
|
|
btn.setAttribute("aria-label", "Wallet — not signed in");
|
|
// If a saved encrypted wallet already exists in this browser (created
|
|
// in a previous session), surface an Unlock option as the first item so
|
|
// users can sign back into the same wallet the mint flow would use —
|
|
// instead of hunting for it inside the Register modal's wallet-choice.
|
|
const hasSaved = (() => {
|
|
try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; }
|
|
})();
|
|
const unlockItem = hasSaved ? `
|
|
<a href="#" role="menuitem" data-action="unlock">
|
|
<span class="mi-ic">🔓</span>
|
|
<span class="mi-body"><b>Unlock my wallet</b><span class="mi-hint">one you already created on this device</span></span>
|
|
</a>
|
|
<hr>
|
|
` : "";
|
|
const headerHint = hasSaved
|
|
? `<span class="profile-hint">Locked wallet on this device</span><span class="profile-sub">Unlock or pick another action</span>`
|
|
: `<span class="profile-hint">No wallet yet</span><span class="profile-sub">Pick how to sign in</span>`;
|
|
// Four direct actions — each opens the shared modal at the matching
|
|
// step (Unlock/Create/Import/WizardConnect) with tabs across the top
|
|
// of the modal so the user can switch between them without going back
|
|
// to a landing screen.
|
|
menu.innerHTML = `
|
|
<div class="profile-header">${headerHint}</div>
|
|
${unlockItem}
|
|
<a href="#" role="menuitem" data-action="new">
|
|
<span class="mi-ic">🆕</span>
|
|
<span class="mi-body"><b>Create a wallet</b><span class="mi-hint">generate a fresh 12-word phrase (sign up)</span></span>
|
|
</a>
|
|
<a href="#" role="menuitem" data-action="import">
|
|
<span class="mi-ic">📥</span>
|
|
<span class="mi-body"><b>Import a wallet</b><span class="mi-hint">seed phrase you already have (sign in)</span></span>
|
|
</a>
|
|
<a href="#" role="menuitem" data-action="wc">
|
|
<span class="mi-ic">🔗</span>
|
|
<span class="mi-body"><b>WizardConnect</b><span class="mi-hint">Cashonize / Paytaca via QR</span></span>
|
|
</a>
|
|
`;
|
|
}
|
|
}
|
|
render();
|
|
|
|
// ---------- open/close ----------
|
|
const open = () => { render(); 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(); });
|
|
|
|
// Cross-tab (storage event fires only in other tabs) + same-tab (portal.html
|
|
// dispatches this after writing/clearing) — either wakes render().
|
|
window.addEventListener("storage", (e) => { if (e.key === "siriusProfile") render(); });
|
|
window.addEventListener("siriusProfileChanged", render);
|
|
|
|
// ---------- menu actions ----------
|
|
// Sign-out clears siriusProfile; the wallet-onboarding items (new/import/wc)
|
|
// open the shared mint modal inline (register-flow.js). If the page did not
|
|
// ship register-flow.js in its own <script>, load it on demand.
|
|
const FLOW_URL = "js/register-flow.js?v=20260907signin";
|
|
async function ensureFlow() {
|
|
if (typeof window.siriusSignInWallet === "function") return true;
|
|
try {
|
|
// Same base as the nav portal anchor — works from root pages and
|
|
// subdirs equally (../ vs ./ handled by BASE).
|
|
await import(BASE + FLOW_URL);
|
|
// Its top-level code exposes the global synchronously after the
|
|
// module graph resolves; still, wait a tick for definiteness.
|
|
for (let i = 0; i < 50 && typeof window.siriusSignInWallet !== "function"; i++) {
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
}
|
|
return typeof window.siriusSignInWallet === "function";
|
|
} catch { return false; }
|
|
}
|
|
|
|
menu.addEventListener("click", async (e) => {
|
|
const t = e.target.closest("[data-action]");
|
|
if (!t) return;
|
|
e.preventDefault();
|
|
const action = t.dataset.action;
|
|
if (action === "signout") {
|
|
try { localStorage.removeItem("siriusProfile"); } catch {}
|
|
window.dispatchEvent(new Event("siriusProfileChanged"));
|
|
// Reload so any signed-in section on the current page (portal, tld) resets.
|
|
location.reload();
|
|
return;
|
|
}
|
|
if (["new", "import", "wc", "unlock"].includes(action)) {
|
|
close();
|
|
// Always route to the portal — it renders the tabbed sign-in INLINE
|
|
// as page content (?mode=X picks the starting tab). Prevents the
|
|
// "modal opens on top of the page" symptom the launcher used to have.
|
|
// Only stay on-page if we're already on portal.html.
|
|
const onPortal = /\/portal\.html(?:$|[?#])/.test(location.pathname + location.search);
|
|
if (onPortal && typeof window.siriusRenderSignInInline === "function") {
|
|
const hasSaved = (() => { try { return !!localStorage.getItem("bns.wallet.v1"); } catch { return false; } })();
|
|
const mode = (action === "unlock" && !hasSaved) ? "import" : action;
|
|
const mount = document.getElementById("signin-inline");
|
|
if (mount) {
|
|
window.siriusRenderSignInInline(mount, mode);
|
|
document.getElementById("signin")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
return;
|
|
}
|
|
}
|
|
// Not on portal (or the inline mount is missing) — take the user
|
|
// there. portal.html?mode=X renders the tabbed sign-in as its main
|
|
// content, no modal.
|
|
location.href = PORTAL_URL + "?mode=" + action;
|
|
}
|
|
});
|
|
|
|
wrap.appendChild(btn);
|
|
wrap.appendChild(menu);
|
|
anchor.replaceWith(wrap);
|
|
|
|
// ---------- inline CSS ----------
|
|
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{
|
|
display:inline-flex;align-items:center;gap:8px;
|
|
cursor:pointer;font-family:inherit;font-size:13.5px;font-weight:500;
|
|
padding:6px 12px 6px 10px;border-radius:999px;
|
|
background:rgba(214,255,61,.10);color:var(--acid);
|
|
border:1px solid rgba(214,255,61,.22);transition:background .1s,border-color .1s
|
|
}
|
|
.topnav .profile-wrap button.portal:hover{background:rgba(214,255,61,.16);border-color:rgba(214,255,61,.35)}
|
|
.topnav .profile-wrap button.portal[aria-expanded="true"]{background:rgba(214,255,61,.20)}
|
|
.topnav .profile-wrap .wallet-dot{
|
|
width:7px;height:7px;border-radius:50%;background:rgba(139,152,169,.45);
|
|
box-shadow:0 0 0 2px rgba(139,152,169,.10)
|
|
}
|
|
.topnav .profile-wrap .wallet-dot.ok{
|
|
background:#4fd1a5;box-shadow:0 0 0 2px rgba(79,209,165,.20)
|
|
}
|
|
.topnav .profile-wrap .wallet-label{font-family:ui-monospace,monospace;font-size:12.5px;letter-spacing:.2px}
|
|
.topnav .profile-wrap .wallet-src{
|
|
font-size:9.5px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;
|
|
padding:2px 6px;border-radius:4px;background:rgba(214,255,61,.15);color:var(--acid);
|
|
margin-left:2px
|
|
}
|
|
.topnav .profile-wrap .wallet-caret{opacity:.6;font-size:11px;margin-left:2px}
|
|
|
|
.topnav .profile-menu{
|
|
position:absolute;right:0;top:calc(100% + 8px);
|
|
min-width:280px;z-index:30;
|
|
background:rgba(18,24,34,.98);backdrop-filter:blur(12px);
|
|
border:1px solid rgba(255,255,255,.12);border-radius:14px;
|
|
padding:8px;display:flex;flex-direction:column;gap:2px;
|
|
box-shadow:0 12px 40px rgba(0,0,0,.55),0 0 0 1px rgba(0,0,0,.4)
|
|
}
|
|
.topnav .profile-menu[hidden]{display:none}
|
|
.topnav .profile-menu > a{
|
|
display:grid;grid-template-columns:24px 1fr;gap:12px;align-items:center;
|
|
padding:9px 11px;border-radius:9px;
|
|
color:#e7eaf1;text-decoration:none;transition:background .08s
|
|
}
|
|
.topnav .profile-menu > a:hover{background:rgba(214,255,61,.10)}
|
|
.topnav .profile-menu > a:hover .mi-ic{filter:brightness(1.2)}
|
|
.topnav .profile-menu .mi-ic{font-size:17px;line-height:1;text-align:center}
|
|
.topnav .profile-menu .mi-body{display:flex;flex-direction:column;gap:1px;font-size:13.5px;font-weight:600;color:#e7eaf1}
|
|
.topnav .profile-menu .mi-hint{font-weight:400;font-size:11.5px;color:#8b98a9;letter-spacing:.1px}
|
|
.topnav .profile-menu hr{border:0;border-top:1px solid rgba(255,255,255,.08);margin:4px 2px}
|
|
.topnav .profile-menu .profile-header{
|
|
padding:8px 11px 12px;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;font-weight:600
|
|
}
|
|
.topnav .profile-menu .profile-sub{color:#5e6678;font-size:12px}
|
|
.topnav .profile-menu code{
|
|
font-family:ui-monospace,monospace;font-size:11.5px;
|
|
color:#e7eaf1;word-break:break-all;background:transparent;padding:0;border:0
|
|
}
|
|
`;
|
|
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) => ({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));
|
|
}
|
|
})();
|