feat(sirius-x/portal): New wallet card, siriusProfile state, ?mode= deep-links
Closes the loop between the nav's profile dropdown and portal.html:
- Adds a 🆕 New wallet card that calls BuiltInWallet.create() to generate
a fresh BIP-39 phrase in the browser. The phrase is shown once for the
user to write down; sign-in only unlocks after they tick the 'I have
written this down' acknowledgement. Copy button included.
- Sign-in (both New and Import paths) now writes localStorage.siriusProfile
= { address, tokenAddress, signedInAt } so any page on the same origin
can render 'signed in' state. Sign-out clears the key and also resets
the New wallet card so a re-sign-in starts clean.
- Handles ?mode=new|import|wc from the profile dropdown's deep-links:
scrolls the matching card into view and briefly outlines it in acid so
the user knows which one they were sent to.
profile-menu.js:
- Re-renders the dropdown on every open() call rather than caching the
first paint, so a sign-in that happens *after* the script's initial
run (same tab: portal.html; other tabs: storage event) reflects in the
nav without needing a full reload.
- Listens for the storage event (cross-tab) and a custom
'siriusProfileChanged' event (same-tab) — portal.html fires that on
every writeProfile/clearProfile call.
This commit is contained in:
parent
e59a2a01b5
commit
6ce9de33a0
2 changed files with 155 additions and 32 deletions
|
|
@ -16,13 +16,15 @@
|
|||
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.
|
||||
// { address, tokenAddress, signedInAt }; clears on sign-out. The read has to
|
||||
// run every time the button label refreshes or the menu opens, because
|
||||
// sign-in can happen on this same page (portal.html) or in another tab
|
||||
// *after* this script's initial run.
|
||||
const readProfile = () => {
|
||||
try { return JSON.parse(localStorage.getItem("siriusProfile") || "null"); } catch { return null; }
|
||||
};
|
||||
const profile = readProfile();
|
||||
|
||||
// Build wrapper + dropdown around the existing pill.
|
||||
// Build wrapper + button + (empty) menu around the existing pill.
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "profile-wrap";
|
||||
const btn = document.createElement("button");
|
||||
|
|
@ -30,37 +32,51 @@
|
|||
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>
|
||||
`;
|
||||
// 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 updates the storage key.
|
||||
function render() {
|
||||
const profile = readProfile();
|
||||
btn.textContent = profile ? "🔑 " + shortAddr(profile.address) + " ▾" : "🔑 Wallet ▾";
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
render();
|
||||
|
||||
// Toggle + close-on-outside.
|
||||
const open = () => { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); };
|
||||
// Toggle + close-on-outside. Re-render on open so a sign-in that happened
|
||||
// after page load (same tab: portal.html; other tab: storage event) shows
|
||||
// up without a full reload.
|
||||
const open = () => { render(); menu.hidden = false; btn.setAttribute("aria-expanded", "true"); };
|
||||
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
|
||||
// Storage events fire only in *other* tabs, but they cover the cross-tab case.
|
||||
// Same-tab: portal.html dispatches "siriusProfileChanged" on window after
|
||||
// it writes/clears the key; render on that too.
|
||||
window.addEventListener("storage", (e) => { if (e.key === "siriusProfile") render(); });
|
||||
window.addEventListener("siriusProfileChanged", render);
|
||||
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(); });
|
||||
|
|
|
|||
119
portal.html
119
portal.html
|
|
@ -137,8 +137,30 @@
|
|||
<p class="muted">Pick one. Nothing is transmitted — resolution is a read-only chain query,
|
||||
and signing (for record edits) happens locally in the browser or in your external wallet.</p>
|
||||
<div class="authbox" style="margin-top:14px">
|
||||
<div class="card">
|
||||
<h3>Import a seed phrase</h3>
|
||||
<div class="card" id="card-new">
|
||||
<h3>🆕 New wallet</h3>
|
||||
<p>Generate a fresh 12-word BIP-39 phrase in your browser. Write it down before continuing —
|
||||
it never leaves this page and there is nothing to reset if you lose it.</p>
|
||||
<div class="row">
|
||||
<button class="btn acid" id="new-btn">Generate a phrase</button>
|
||||
</div>
|
||||
<div id="new-phrase" class="hidden" style="margin-top:14px">
|
||||
<label>Your new recovery phrase</label>
|
||||
<textarea id="new-seed" readonly style="font-family:ui-monospace,monospace;font-size:14px"></textarea>
|
||||
<label style="margin-top:10px;display:flex;gap:9px;align-items:flex-start;font-size:13.5px">
|
||||
<input type="checkbox" id="new-ack" style="margin-top:3px">
|
||||
<span>I have written this phrase down somewhere safe. I understand it is the only way
|
||||
to recover this wallet and cannot be reset.</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<button class="btn acid" id="new-signin-btn" disabled>Sign in with new wallet →</button>
|
||||
<button class="btn ghost" id="new-copy-btn">Copy phrase</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="new-err" class="err hidden"></div>
|
||||
</div>
|
||||
<div class="card" id="card-import">
|
||||
<h3>📥 Import a seed phrase</h3>
|
||||
<p>Paste your 12- or 24-word BIP-39 recovery phrase. It stays in memory only, wiped when you
|
||||
close the tab or sign out.</p>
|
||||
<label for="seed">Recovery phrase</label>
|
||||
|
|
@ -149,8 +171,8 @@
|
|||
</div>
|
||||
<div id="signin-err" class="err hidden"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Connect an external wallet</h3>
|
||||
<div class="card" id="card-wc">
|
||||
<h3>🔗 Connect an external wallet</h3>
|
||||
<p>Use Cashonize 0.9+ or Paytaca via WizardConnect. The wallet signs record edits — the seed
|
||||
never touches this page.</p>
|
||||
<div class="row">
|
||||
|
|
@ -158,7 +180,8 @@
|
|||
<span class="badge b-warn" style="margin-left:6px">soon</span></button>
|
||||
</div>
|
||||
<p class="dim" style="margin-top:8px">Portal-side WizardConnect wiring is in progress; the
|
||||
register.html flow already ships the WizardConnect adapter, and the portal will reuse it.</p>
|
||||
<a href="./register.html">register.html</a> flow already ships the WizardConnect adapter, and
|
||||
the portal will reuse it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status" style="margin-top:14px">
|
||||
|
|
@ -270,6 +293,27 @@ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<"
|
|||
|
||||
let wallet = null;
|
||||
|
||||
// Persist signed-in state so the profile dropdown (profile-menu.js) can flip
|
||||
// from "onboarding" to "signed in" and any tab on the same origin (sirius.x)
|
||||
// reflects the current session. The wallet keys themselves never leave this
|
||||
// page — localStorage only holds a small address-only breadcrumb, no seed.
|
||||
function writeProfile(w) {
|
||||
try {
|
||||
localStorage.setItem("siriusProfile", JSON.stringify({
|
||||
address: w.address,
|
||||
tokenAddress: w.tokenAddress,
|
||||
signedInAt: Date.now(),
|
||||
}));
|
||||
} catch {}
|
||||
// profile-menu.js listens for this to re-render the nav dropdown in the
|
||||
// same tab (the "storage" event only fires in *other* tabs).
|
||||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||||
}
|
||||
function clearProfile() {
|
||||
try { localStorage.removeItem("siriusProfile"); } catch {}
|
||||
window.dispatchEvent(new Event("siriusProfileChanged"));
|
||||
}
|
||||
|
||||
// ---------- sign in ----------
|
||||
$("signin-btn").addEventListener("click", async () => {
|
||||
const btn = $("signin-btn");
|
||||
|
|
@ -286,20 +330,83 @@ $("signin-btn").addEventListener("click", async () => {
|
|||
btn.disabled = false; btn.textContent = "Sign in →";
|
||||
return;
|
||||
}
|
||||
// Wipe the textarea immediately — the seed is now in the wallet object only.
|
||||
$("seed").value = "";
|
||||
btn.disabled = false; btn.textContent = "Sign in →";
|
||||
writeProfile(wallet);
|
||||
await enterPortal();
|
||||
});
|
||||
|
||||
// ---------- new wallet ----------
|
||||
// Generates a fresh BIP-39 phrase in the browser via BuiltInWallet.create().
|
||||
// The phrase is shown once for the user to write down; sign-in only unlocks
|
||||
// after they acknowledge they have saved it. From that point on the flow is
|
||||
// identical to import (BuiltInWallet in memory, no persistence of the seed).
|
||||
$("new-btn").addEventListener("click", async () => {
|
||||
const btn = $("new-btn");
|
||||
const errBox = $("new-err");
|
||||
errBox.classList.add("hidden");
|
||||
btn.disabled = true; btn.textContent = "Generating…";
|
||||
let fresh;
|
||||
try { fresh = await BNS.BuiltInWallet.create(); }
|
||||
catch (e) {
|
||||
errBox.textContent = "Could not generate a phrase: " + (e.message || e);
|
||||
errBox.classList.remove("hidden");
|
||||
btn.disabled = false; btn.textContent = "Generate a phrase";
|
||||
return;
|
||||
}
|
||||
wallet = fresh;
|
||||
$("new-seed").value = fresh.mnemonic;
|
||||
$("new-phrase").classList.remove("hidden");
|
||||
btn.disabled = true; btn.textContent = "Phrase generated";
|
||||
});
|
||||
|
||||
$("new-ack").addEventListener("change", (e) => {
|
||||
$("new-signin-btn").disabled = !e.target.checked;
|
||||
});
|
||||
|
||||
$("new-copy-btn").addEventListener("click", () => {
|
||||
navigator.clipboard?.writeText($("new-seed").value);
|
||||
});
|
||||
|
||||
$("new-signin-btn").addEventListener("click", async () => {
|
||||
if (!wallet) return;
|
||||
// Wipe the visible phrase immediately — it's in the wallet object now.
|
||||
$("new-seed").value = "";
|
||||
$("new-phrase").classList.add("hidden");
|
||||
writeProfile(wallet);
|
||||
await enterPortal();
|
||||
});
|
||||
|
||||
$("signout-btn").addEventListener("click", () => {
|
||||
wallet = null;
|
||||
clearProfile();
|
||||
$("me").classList.add("hidden");
|
||||
$("signin").classList.remove("hidden");
|
||||
$("names-list").innerHTML = "";
|
||||
$("names-status").textContent = "Loading names from the chain…";
|
||||
// Reset the new-wallet card so a subsequent sign-in starts clean.
|
||||
$("new-btn").disabled = false; $("new-btn").textContent = "Generate a phrase";
|
||||
$("new-phrase").classList.add("hidden");
|
||||
$("new-ack").checked = false;
|
||||
$("new-signin-btn").disabled = true;
|
||||
});
|
||||
|
||||
// ---------- ?mode= deep-link ----------
|
||||
// The profile dropdown links to portal.html?mode=new|import|wc — bring the
|
||||
// matching card into view and highlight its border briefly so the user knows
|
||||
// which one they were sent to.
|
||||
(function handleMode() {
|
||||
const mode = new URLSearchParams(location.search).get("mode");
|
||||
if (!mode) return;
|
||||
const id = { new: "card-new", import: "card-import", wc: "card-wc" }[mode];
|
||||
const el = id && document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.style.borderColor = "var(--acid)";
|
||||
el.style.boxShadow = "0 0 0 3px rgba(214,255,61,.15)";
|
||||
setTimeout(() => el.scrollIntoView({ behavior: "smooth", block: "center" }), 50);
|
||||
setTimeout(() => { el.style.borderColor = ""; el.style.boxShadow = ""; }, 3000);
|
||||
})();
|
||||
|
||||
async function enterPortal() {
|
||||
$("signin").classList.add("hidden");
|
||||
$("me").classList.remove("hidden");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue