fix(aegis): 0.9.6 — adding a wallet asks create-or-import

"+ Add another BCH" created a fresh wallet on the spot. The old comment
argued that being inside a coin's address list made the intent
unambiguous; it isn't. "Add another BCH wallet" is just as often "bring
in the one I already have somewhere else", and guessing wrong is not
harmless — the user gets an empty new address and has to work out for
themselves why their funds aren't in it.

Adds aegisChoose(), a pick-one sibling of aegisConfirm for branches
where the honest answer is a question rather than a yes/no, and puts it
in front of every path that reached addWallet:

- + Add another <TICKER> in the coin drilldown
- + Add on an unowned coin in the browse picker (now routes to the
  existing New / Import / Connect chooser, with the coin still
  preselected through whichever branch is taken)
- + Add your first wallet on the empty state — the most important one,
  since someone arriving with an existing seed was being handed a
  create-only flow

The empty-state copy claimed "there's no separate seed to import",
which stopped being true when imports shipped and actively told users
the feature they wanted did not exist.

Verified in a rendered panel: the chooser appears, addWallet is not
called until Create is picked, Import opens the import modal, and
Cancel does nothing.
This commit is contained in:
Local Dev 2026-09-23 08:33:29 +02:00
parent 7b9049f6fc
commit f07df2b39e
2 changed files with 82 additions and 10 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.9.5", "version": "0.9.6",
"category": "plugin", "category": "plugin",
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.",
"author": "Silent Mode", "author": "Silent Mode",

View file

@ -595,10 +595,12 @@ function fillPicker() {
d.querySelectorAll("[data-browse-add]").forEach((row) => row.addEventListener("click", (e) => { d.querySelectorAll("[data-browse-add]").forEach((row) => row.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
const chain = row.dataset.browseAdd; const chain = row.dataset.browseAdd;
// Jump straight into the create-wallet flow for that coin. The // Offer the method chooser (New / Import / Connect) rather than
// Add pane keys off `pickerTab = "add"` and reads the target // dropping straight into create — "add a BCH wallet" is just as
// chain from a scratch field the coin-picker fills. // often "bring in the one I already have". pickerAddChain is
pickerTab = "add"; // module-level, so the coin stays preselected through whichever
// branch the user picks.
pickerTab = "method";
pickerAddChain = chain; pickerAddChain = chain;
fillPicker(); fillPicker();
})); }));
@ -880,6 +882,56 @@ function aegisConfirm(opts) {
}); });
} }
// Pick-one-of-several sibling of aegisConfirm, for branches where the
// honest answer is a question rather than a yes/no. Resolves the chosen
// option's `id`, or null if dismissed.
// opts: { title, body, icon, options: [{id, label, hint, primary}] }
function aegisChoose(opts) {
const o = opts || {};
const options = Array.isArray(o.options) ? o.options : [];
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:flex-start;justify-content:center;z-index:100000;padding-top:24px";
overlay.innerHTML = `
<div style="width:min(94vw,360px);background:var(--panel,#12161e);border:1px solid var(--line,#2a2f38);border-radius:10px;padding:14px 14px 12px;box-shadow:0 10px 40px rgba(0,0,0,.4)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-size:17px;line-height:1">${o.icon || "+"}</span>
<div style="font-weight:600;flex:1">${esc(o.title || "Choose")}</div>
</div>
${o.body ? `<div class="hint" style="margin-bottom:12px;line-height:1.5">${o.body}</div>` : ""}
<div style="display:flex;flex-direction:column;gap:6px">
${options.map((op) => `
<button class="btn ${op.primary ? "primary" : ""}" data-ac="${esc(op.id)}"
style="text-align:left;padding:9px 11px;display:block;width:100%">
<div style="font-weight:600">${esc(op.label)}</div>
${op.hint ? `<div class="hint" style="margin-top:2px;font-weight:400">${esc(op.hint)}</div>` : ""}
</button>`).join("")}
</div>
<div class="actions" style="justify-content:flex-end;margin-top:10px">
<button class="btn" data-ac="">${esc(o.cancelLabel || "Cancel")}</button>
</div>
</div>`;
document.body.appendChild(overlay);
let done = false;
const finish = (val) => {
if (done) return;
done = true;
document.removeEventListener("keydown", onKey, true);
try { overlay.remove(); } catch {}
resolve(val);
};
const onKey = (e) => { if (e.key === "Escape") { e.stopPropagation(); finish(null); } };
document.addEventListener("keydown", onKey, true);
overlay.addEventListener("click", (e) => { if (e.target === overlay) finish(null); });
overlay.querySelectorAll("[data-ac]").forEach((b) => b.addEventListener("click", (e) => {
e.stopPropagation();
finish(b.dataset.ac || null);
}));
const first = overlay.querySelector('[data-ac]:not([data-ac=""])');
if (first) first.focus();
});
}
// Single-button sibling of aegisConfirm, for the error notices that used // Single-button sibling of aegisConfirm, for the error notices that used
// window.alert(). Fire-and-forget: callers don't need the result, so it // window.alert(). Fire-and-forget: callers don't need the result, so it
// works from sync handlers too. // works from sync handlers too.
@ -2597,10 +2649,26 @@ function renderInlineCoinList(el, groupKey, group) {
// the coin list when the last address under a coin goes away. // the coin list when the last address under a coin goes away.
el.querySelector("#stripAddMore").addEventListener("click", async () => { el.querySelector("#stripAddMore").addEventListener("click", async () => {
// Add another wallet of the same coin+network directly, without // This used to create a fresh wallet immediately, on the reasoning
// opening the picker sheet — the user is already inside this coin's // that being inside a coin's address list made the intent
// address list so their intent is unambiguous. // unambiguous. It doesn't: "add another BCH wallet" is just as often
// "bring in the one I already have somewhere else". Creating instead
// of importing is not a harmless guess either — the user ends up with
// an empty new address and has to work out why their funds aren't
// there. So ask.
const first = gw[0]; const first = gw[0];
const pick = await aegisChoose({
title: `Add another ${meta.ticker} wallet`,
// meta.coinName already carries the network ("TRX Nile"), so naming
// networkLabel again read "TRX Nile · Nile testnet".
body: `On <b>${esc(meta.coinName)}</b>.`,
options: [
{ id: "create", label: "Create a new wallet", hint: "Derived from your Theseus vault — nothing to write down", primary: true },
{ id: "import", label: "Import an existing wallet", hint: "BIP39 mnemonic, or a chain-native private key" },
],
});
if (!pick) return;
if (pick === "import") { openImportModal(first.chain); return; }
try { try {
state = await S.invoke("addWallet", { chain: first.chain, network: first.network }); state = await S.invoke("addWallet", { chain: first.chain, network: first.network });
settingsFilled = false; render(); settingsFilled = false; render();
@ -2869,7 +2937,7 @@ function render() {
if (!ready && !fullLock) { if (!ready && !fullLock) {
const copy = { const copy = {
error: ["⚠", "This wallet could not start.", s?.error || ""], error: ["⚠", "This wallet could not start.", s?.error || ""],
empty: ["🛡", "No wallets yet.", "Aegis derives every wallet from your Theseus password vault — there's no separate seed to import. Pick a coin below to create your first one."], empty: ["🛡", "No wallets yet.", "Aegis can derive a fresh wallet from your Theseus password vault — nothing extra to write down — or import one you already have from its seed phrase or private key."],
}[phase] || ["…", "Starting…", ""]; }[phase] || ["…", "Starting…", ""];
let form = ""; let form = "";
if (phase === "empty") { if (phase === "empty") {
@ -2880,7 +2948,11 @@ function render() {
const btn = $("gateAddWallet"); const btn = $("gateAddWallet");
if (btn) btn.addEventListener("click", () => { if (btn) btn.addEventListener("click", () => {
const d = $("drop"); const d = $("drop");
pickerTab = "add"; // First-run is the MOST important place to offer import: someone
// arriving with an existing seed who is handed a create-only flow
// ends up staring at an empty wallet wondering where their coins
// went. Method chooser, not straight to create.
pickerTab = "method";
d.hidden = false; d.hidden = false;
fillPicker(); fillPicker();
}); });