fix(theseus/aegis): retire bchwallet on every launch + branded dock/list icon

Two follow-ups from the on-device test.

Two Aegis addons showing up (bchwallet + aegis):
- migrateAegisRename previously only ran when addons/aegis/ didn't exist,
  which meant any bchwallet copy the signed OTA update endpoint reinstalls
  after the first migration stays there forever, and AddonHost loads both
  as separate wallets. Rewritten to always retire addons/bchwallet/ when
  it's present, regardless of whether aegis/ is already installed. The
  storage-copy (bchwallet.json → aegis.json) still only runs the first
  time so a downgrade doesn't clobber fresh 0.4+ state.
- Also flushes any stray addons/siawallet/ that comes back the same way.

Sidebar dock and Extensions list icons showed the raw 🛡 emoji:
- chrome.html's dock-button renderer and settings.html's extensions-list
  renderer now accept `data:image/svg+xml…` values for manifest.icon and
  render them as <img> instead of text. Emoji strings still render as
  before.
- aegis addon.json's icon is now the exact hex-aspis mark from
  aegis.x/brand/favicon.svg (URL-encoded inline). Version bumped to
  0.4.1 so seedBundledAddons reseeds the new addon.json on next launch.
This commit is contained in:
Local Dev 2026-09-08 18:42:00 +02:00
parent 7d580be598
commit 22be28ed17
4 changed files with 35 additions and 10 deletions

View file

@ -1,10 +1,10 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.4.0", "version": "0.4.1",
"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",
"icon": "🛡", "icon": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cpolygon points='16,2 28,9 28,23 16,30 4,23 4,9' fill='%230a0a0d' stroke='%23D6FF3D' stroke-width='1.6' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='none' stroke='%23D6FF3D' stroke-width='1.4'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23D6FF3D'/%3E%3C/svg%3E",
"main": "index.js", "main": "index.js",
"capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal"], "capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal"],
"absorbs": ["bchwallet", "siawallet"], "absorbs": ["bchwallet", "siawallet"],

View file

@ -576,7 +576,18 @@
const dataAttr = b.kind === "panel" const dataAttr = b.kind === "panel"
? `data-panel="${b.panelId.replace(/"/g,"&quot;")}"` ? `data-panel="${b.panelId.replace(/"/g,"&quot;")}"`
: `data-menu="${b.addonId.replace(/"/g,"&quot;")}"`; : `data-menu="${b.addonId.replace(/"/g,"&quot;")}"`;
return `<button class="extbtn${cls}" ${dataAttr} title="${b.title.replace(/"/g,"&quot;")}">${b.icon.replace(/</g,"&lt;")}</button>`; // Icon is text by default (an emoji like 🛡, or a "•" fallback). An
// add-on that wants a branded mark can set manifest.icon to a data:
// URI (svg or png) and we render it as an <img> instead — same size
// slot, no layout change. Anything else is HTML-escaped and stays
// text so the emoji case keeps working.
let iconHtml;
if (/^data:image\//i.test(b.icon)) {
iconHtml = `<img src="${b.icon.replace(/"/g,"&quot;")}" alt="" style="width:18px;height:18px;display:block;margin:0 auto">`;
} else {
iconHtml = b.icon.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
return `<button class="extbtn${cls}" ${dataAttr} title="${b.title.replace(/"/g,"&quot;")}">${iconHtml}</button>`;
}).join(""); }).join("");
extButtons.querySelectorAll(".extbtn").forEach((btn) => { extButtons.querySelectorAll(".extbtn").forEach((btn) => {
btn.onclick = () => { btn.onclick = () => {

12
main.js
View file

@ -1355,13 +1355,21 @@ function migrateAegisRename() {
const aegisDir = path.join(addonsRoot, "aegis"); const aegisDir = path.join(addonsRoot, "aegis");
const bchJson = path.join(dataRoot, "bchwallet.json"); const bchJson = path.join(dataRoot, "bchwallet.json");
const aegisJson = path.join(dataRoot, "aegis.json"); const aegisJson = path.join(dataRoot, "aegis.json");
if (fs.existsSync(bchDir) && !fs.existsSync(aegisDir)) { // Copy legacy storage into the new file exactly once, only when the new
// file doesn't exist yet (a downgrade to 0.3.x would otherwise clobber
// fresh state written by 0.4+).
if (fs.existsSync(bchJson) && !fs.existsSync(aegisJson)) { if (fs.existsSync(bchJson) && !fs.existsSync(aegisJson)) {
try { fs.copyFileSync(bchJson, aegisJson); console.log("[addons] migrated bchwallet.json -> aegis.json"); } try { fs.copyFileSync(bchJson, aegisJson); console.log("[addons] migrated bchwallet.json -> aegis.json"); }
catch (err) { console.warn("[addons] migrate storage failed:", err?.message); } catch (err) { console.warn("[addons] migrate storage failed:", err?.message); }
} }
// Retire the bchwallet folder EVERY LAUNCH it exists. The signed OTA
// update endpoint may still advertise `id=bchwallet` updates, and
// promoteStagedUpdates (which runs before us) could reinstall it. Left
// active it would load as a second Aegis under a stale id, doubling
// every wallet in the sidebar.
if (fs.existsSync(bchDir)) {
const backup = path.join(backups, `bchwallet-migrated-${stamp}`); const backup = path.join(backups, `bchwallet-migrated-${stamp}`);
try { fs.renameSync(bchDir, backup); console.log(`[addons] retired addons/bchwallet -> addons-backups/${path.basename(backup)} (folder renamed to aegis/)`); } try { fs.renameSync(bchDir, backup); console.log(`[addons] retired addons/bchwallet -> addons-backups/${path.basename(backup)}${fs.existsSync(aegisDir) ? " (aegis already present)" : " (folder renamed to aegis/)"}`); }
catch (err) { console.warn("[addons] retire bchwallet failed:", err?.message); } catch (err) { console.warn("[addons] retire bchwallet failed:", err?.message); }
} }

View file

@ -1219,7 +1219,13 @@
const builtInBadge = isBundled const builtInBadge = isBundled
? '<span style="display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle">BUILT-IN</span>' ? '<span style="display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle">BUILT-IN</span>'
: ""; : "";
return '<div class="row"><div class="txt"><div class="t">' + a.icon + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div></div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>'; // Manifest icons may be an emoji ("🛡"), a "•" fallback, or a
// data: URI to a branded SVG/PNG — render as <img> for URIs,
// as escaped text otherwise so the emoji case keeps working.
const iconHtml = /^data:image\//i.test(a.icon || "")
? '<img src="' + escapeAttr(a.icon) + '" alt="" style="width:16px;height:16px;vertical-align:middle;margin-right:4px">'
: escapeHtml(a.icon || "•");
return '<div class="row"><div class="txt"><div class="t">' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div></div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
}; };
const sectionHeader = (label, hint) => const sectionHeader = (label, hint) =>
'<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>' '<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>'