All checks were successful
check / check (push) Successful in 13s
Replace jargon-heavy terminal-style UI with plain-language views. New data model: wallets (HD or private key) contain addresses. Main view lists all addresses grouped by wallet with balances. HD wallets get a "+" to add addresses; key wallets have one. Two import paths: recovery phrase and private key. All labels use plain English, full-sentence errors, inline help text. README updated with full UI philosophy, language guide, data model, and navigation docs.
523 lines
16 KiB
JavaScript
523 lines
16 KiB
JavaScript
// AutistMask popup UI — view management and event wiring
|
|
// All wallet logic will live in background/; this file is purely UI.
|
|
|
|
const VIEWS = [
|
|
"lock",
|
|
"welcome",
|
|
"create",
|
|
"import-phrase",
|
|
"import-key",
|
|
"main",
|
|
"add-wallet",
|
|
"address",
|
|
"send",
|
|
"receive",
|
|
"add-token",
|
|
"settings",
|
|
"approve",
|
|
];
|
|
|
|
function showView(name) {
|
|
for (const v of VIEWS) {
|
|
const el = document.getElementById(`view-${v}`);
|
|
if (el) {
|
|
el.classList.toggle("hidden", v !== name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- mock state (will be replaced by background messaging) --
|
|
// A wallet is either { type: "hd", name, mnemonic, addresses: [...] }
|
|
// or { type: "key", name, privateKey, addresses: [single] }.
|
|
// Each address is { address, balance, tokens: [...] }.
|
|
const state = {
|
|
locked: true,
|
|
hasWallet: false,
|
|
password: null,
|
|
wallets: [],
|
|
selectedWallet: null,
|
|
selectedAddress: null,
|
|
rpcUrl: "https://eth.llamarpc.com",
|
|
isFirstSetup: true,
|
|
};
|
|
|
|
// -- helpers --
|
|
function $(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function showError(id, msg) {
|
|
const el = $(id);
|
|
el.textContent = msg;
|
|
el.classList.remove("hidden");
|
|
}
|
|
|
|
function hideError(id) {
|
|
$(id).classList.add("hidden");
|
|
}
|
|
|
|
function truncateAddress(addr) {
|
|
if (!addr) return "";
|
|
return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
|
|
}
|
|
|
|
function makeStubAddress() {
|
|
const hex = Array.from({ length: 40 }, () =>
|
|
Math.floor(Math.random() * 16).toString(16),
|
|
).join("");
|
|
return {
|
|
address: "0x" + hex,
|
|
balance: "0.0000",
|
|
tokens: [],
|
|
};
|
|
}
|
|
|
|
// -- render wallet list on main view --
|
|
function renderWalletList() {
|
|
const container = $("wallet-list");
|
|
if (state.wallets.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
state.wallets.forEach((wallet, wi) => {
|
|
html += `<div class="mb-3">`;
|
|
html += `<div class="flex justify-between items-center border-b border-border pb-1 mb-1">`;
|
|
html += `<span class="font-bold">${wallet.name}</span>`;
|
|
if (wallet.type === "hd") {
|
|
html += `<button class="btn-add-address border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-wallet="${wi}" title="Add another address to this wallet">+</button>`;
|
|
}
|
|
html += `</div>`;
|
|
|
|
wallet.addresses.forEach((addr, ai) => {
|
|
html += `<div class="address-row flex justify-between items-center py-1 border-b border-border-light cursor-pointer hover:bg-hover px-1" data-wallet="${wi}" data-address="${ai}">`;
|
|
html += `<span class="text-xs">${truncateAddress(addr.address)}</span>`;
|
|
html += `<span class="text-xs">${addr.balance} ETH</span>`;
|
|
html += `</div>`;
|
|
});
|
|
|
|
html += `</div>`;
|
|
});
|
|
container.innerHTML = html;
|
|
|
|
// bind clicks on address rows
|
|
container.querySelectorAll(".address-row").forEach((row) => {
|
|
row.addEventListener("click", () => {
|
|
state.selectedWallet = parseInt(row.dataset.wallet, 10);
|
|
state.selectedAddress = parseInt(row.dataset.address, 10);
|
|
showAddressDetail();
|
|
});
|
|
});
|
|
|
|
// bind clicks on + buttons within HD wallets
|
|
container.querySelectorAll(".btn-add-address").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const wi = parseInt(btn.dataset.wallet, 10);
|
|
state.wallets[wi].addresses.push(makeStubAddress());
|
|
renderWalletList();
|
|
});
|
|
});
|
|
}
|
|
|
|
function showAddressDetail() {
|
|
const wallet = state.wallets[state.selectedWallet];
|
|
const addr = wallet.addresses[state.selectedAddress];
|
|
$("address-title").textContent = wallet.name;
|
|
$("address-full").textContent = addr.address;
|
|
$("address-copied-msg").textContent = "";
|
|
$("address-eth-balance").textContent = addr.balance;
|
|
$("address-usd-value").textContent = "";
|
|
updateTokenList(addr);
|
|
updateSendTokenSelect(addr);
|
|
showView("address");
|
|
}
|
|
|
|
function updateTokenList(addr) {
|
|
const list = $("token-list");
|
|
if (addr.tokens.length === 0) {
|
|
list.innerHTML =
|
|
'<div class="text-muted text-xs py-1">No tokens added yet. Use "+ Add" to track a token.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = addr.tokens
|
|
.map(
|
|
(t) =>
|
|
`<div class="py-1 border-b border-border-light flex justify-between">` +
|
|
`<span>${t.symbol}</span>` +
|
|
`<span>${t.balance || "0"}</span>` +
|
|
`</div>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function updateSendTokenSelect(addr) {
|
|
const sel = $("send-token");
|
|
sel.innerHTML = '<option value="ETH">ETH</option>';
|
|
addr.tokens.forEach((t) => {
|
|
const opt = document.createElement("option");
|
|
opt.value = t.contractAddress;
|
|
opt.textContent = t.symbol;
|
|
sel.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
function currentAddress() {
|
|
if (state.selectedWallet === null || state.selectedAddress === null) {
|
|
return null;
|
|
}
|
|
return state.wallets[state.selectedWallet].addresses[state.selectedAddress];
|
|
}
|
|
|
|
function addWalletAndGoToMain(wallet) {
|
|
state.wallets.push(wallet);
|
|
state.hasWallet = true;
|
|
state.isFirstSetup = false;
|
|
renderWalletList();
|
|
showView("main");
|
|
}
|
|
|
|
function showImportView(type) {
|
|
if (type === "phrase") {
|
|
$("import-mnemonic").value = "";
|
|
hideError("import-phrase-error");
|
|
const needsPw = state.isFirstSetup;
|
|
$("import-phrase-password-section").classList.toggle(
|
|
"hidden",
|
|
!needsPw,
|
|
);
|
|
$("import-phrase-password-confirm-section").classList.toggle(
|
|
"hidden",
|
|
!needsPw,
|
|
);
|
|
showView("import-phrase");
|
|
} else {
|
|
$("import-private-key").value = "";
|
|
hideError("import-key-error");
|
|
const needsPw = state.isFirstSetup;
|
|
$("import-key-password-section").classList.toggle("hidden", !needsPw);
|
|
$("import-key-password-confirm-section").classList.toggle(
|
|
"hidden",
|
|
!needsPw,
|
|
);
|
|
showView("import-key");
|
|
}
|
|
}
|
|
|
|
function showCreateView() {
|
|
// TODO: generate real mnemonic via background
|
|
$("create-mnemonic").textContent =
|
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
hideError("create-error");
|
|
showView("create");
|
|
}
|
|
|
|
function validatePasswords(pwId, pw2Id, errorId) {
|
|
if (!state.isFirstSetup) return true;
|
|
const pw = $(pwId).value;
|
|
const pw2 = $(pw2Id).value;
|
|
if (!pw) {
|
|
showError(errorId, "Please choose a password.");
|
|
return false;
|
|
}
|
|
if (pw.length < 8) {
|
|
showError(errorId, "Password must be at least 8 characters.");
|
|
return false;
|
|
}
|
|
if (pw !== pw2) {
|
|
showError(errorId, "Passwords do not match.");
|
|
return false;
|
|
}
|
|
state.password = pw;
|
|
return true;
|
|
}
|
|
|
|
// -- init --
|
|
function init() {
|
|
if (!state.hasWallet) {
|
|
showView("welcome");
|
|
} else if (state.locked) {
|
|
showView("lock");
|
|
} else {
|
|
renderWalletList();
|
|
showView("main");
|
|
}
|
|
|
|
// -- Lock screen --
|
|
$("btn-unlock").addEventListener("click", () => {
|
|
const pw = $("unlock-password").value;
|
|
if (!pw) {
|
|
showError("unlock-error", "Please enter your password.");
|
|
return;
|
|
}
|
|
hideError("unlock-error");
|
|
// TODO: send unlock message to background
|
|
state.locked = false;
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
// -- Welcome --
|
|
$("btn-welcome-new").addEventListener("click", showCreateView);
|
|
$("btn-welcome-recovery").addEventListener("click", () =>
|
|
showImportView("phrase"),
|
|
);
|
|
$("btn-welcome-key").addEventListener("click", () => showImportView("key"));
|
|
|
|
// -- Create wallet --
|
|
$("btn-create-confirm").addEventListener("click", () => {
|
|
if (
|
|
!validatePasswords(
|
|
"create-password",
|
|
"create-password-confirm",
|
|
"create-error",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
hideError("create-error");
|
|
const walletNum = state.wallets.length + 1;
|
|
addWalletAndGoToMain({
|
|
type: "hd",
|
|
name: "Wallet " + walletNum,
|
|
mnemonic: $("create-mnemonic").textContent,
|
|
addresses: [makeStubAddress()],
|
|
});
|
|
});
|
|
|
|
$("btn-create-back").addEventListener("click", () => {
|
|
showView(state.isFirstSetup ? "welcome" : "add-wallet");
|
|
});
|
|
|
|
// -- Import recovery phrase --
|
|
$("btn-import-phrase-confirm").addEventListener("click", () => {
|
|
const mnemonic = $("import-mnemonic").value.trim();
|
|
if (!mnemonic) {
|
|
showError(
|
|
"import-phrase-error",
|
|
"Please enter your recovery phrase.",
|
|
);
|
|
return;
|
|
}
|
|
const words = mnemonic.split(/\s+/);
|
|
if (words.length !== 12 && words.length !== 24) {
|
|
showError(
|
|
"import-phrase-error",
|
|
"Recovery phrase must be 12 or 24 words. You entered " +
|
|
words.length +
|
|
".",
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
!validatePasswords(
|
|
"import-phrase-password",
|
|
"import-phrase-password-confirm",
|
|
"import-phrase-error",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
hideError("import-phrase-error");
|
|
const walletNum = state.wallets.length + 1;
|
|
addWalletAndGoToMain({
|
|
type: "hd",
|
|
name: "Wallet " + walletNum,
|
|
mnemonic: mnemonic,
|
|
addresses: [makeStubAddress()],
|
|
});
|
|
});
|
|
|
|
$("btn-import-phrase-back").addEventListener("click", () => {
|
|
showView(state.isFirstSetup ? "welcome" : "add-wallet");
|
|
});
|
|
|
|
// -- Import private key --
|
|
$("btn-import-key-confirm").addEventListener("click", () => {
|
|
const key = $("import-private-key").value.trim();
|
|
if (!key) {
|
|
showError("import-key-error", "Please enter your private key.");
|
|
return;
|
|
}
|
|
if (
|
|
!validatePasswords(
|
|
"import-key-password",
|
|
"import-key-password-confirm",
|
|
"import-key-error",
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
hideError("import-key-error");
|
|
const walletNum = state.wallets.length + 1;
|
|
addWalletAndGoToMain({
|
|
type: "key",
|
|
name: "Wallet " + walletNum,
|
|
privateKey: key,
|
|
addresses: [makeStubAddress()],
|
|
});
|
|
});
|
|
|
|
$("btn-import-key-back").addEventListener("click", () => {
|
|
showView(state.isFirstSetup ? "welcome" : "add-wallet");
|
|
});
|
|
|
|
// -- Main view --
|
|
$("btn-lock").addEventListener("click", () => {
|
|
state.locked = true;
|
|
$("unlock-password").value = "";
|
|
showView("lock");
|
|
});
|
|
|
|
$("btn-settings").addEventListener("click", () => {
|
|
$("settings-rpc").value = state.rpcUrl;
|
|
showView("settings");
|
|
});
|
|
|
|
$("btn-add-wallet").addEventListener("click", () => {
|
|
showView("add-wallet");
|
|
});
|
|
|
|
// -- Add wallet menu (from main view) --
|
|
$("btn-add-wallet-new").addEventListener("click", showCreateView);
|
|
$("btn-add-wallet-phrase").addEventListener("click", () =>
|
|
showImportView("phrase"),
|
|
);
|
|
$("btn-add-wallet-key").addEventListener("click", () =>
|
|
showImportView("key"),
|
|
);
|
|
$("btn-add-wallet-back").addEventListener("click", () => {
|
|
showView("main");
|
|
});
|
|
|
|
// -- Address detail --
|
|
$("address-full").addEventListener("click", () => {
|
|
const addr = $("address-full").textContent;
|
|
if (addr) {
|
|
navigator.clipboard.writeText(addr);
|
|
$("address-copied-msg").textContent = "Copied!";
|
|
setTimeout(() => {
|
|
$("address-copied-msg").textContent = "";
|
|
}, 2000);
|
|
}
|
|
});
|
|
|
|
$("btn-address-back").addEventListener("click", () => {
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
$("btn-send").addEventListener("click", () => {
|
|
$("send-to").value = "";
|
|
$("send-amount").value = "";
|
|
$("send-fee-estimate").classList.add("hidden");
|
|
$("send-status").classList.add("hidden");
|
|
showView("send");
|
|
});
|
|
|
|
$("btn-receive").addEventListener("click", () => {
|
|
const addr = currentAddress();
|
|
$("receive-address").textContent = addr ? addr.address : "";
|
|
showView("receive");
|
|
});
|
|
|
|
$("btn-add-token").addEventListener("click", () => {
|
|
$("add-token-address").value = "";
|
|
$("add-token-info").classList.add("hidden");
|
|
hideError("add-token-error");
|
|
showView("add-token");
|
|
});
|
|
|
|
// -- Send --
|
|
$("btn-send-confirm").addEventListener("click", () => {
|
|
const to = $("send-to").value.trim();
|
|
const amount = $("send-amount").value.trim();
|
|
if (!to) {
|
|
showError("send-status", "Please enter a recipient address.");
|
|
$("send-status").classList.remove("hidden");
|
|
return;
|
|
}
|
|
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
|
|
showError("send-status", "Please enter a valid amount.");
|
|
$("send-status").classList.remove("hidden");
|
|
return;
|
|
}
|
|
// TODO: construct and send transaction via background
|
|
const el = $("send-status");
|
|
el.textContent = "Sent! (stub)";
|
|
el.classList.remove("hidden");
|
|
});
|
|
|
|
$("btn-send-back").addEventListener("click", () => {
|
|
showAddressDetail();
|
|
});
|
|
|
|
// -- Receive --
|
|
$("btn-receive-copy").addEventListener("click", () => {
|
|
const addr = $("receive-address").textContent;
|
|
if (addr) {
|
|
navigator.clipboard.writeText(addr);
|
|
}
|
|
});
|
|
|
|
$("btn-receive-back").addEventListener("click", () => {
|
|
showAddressDetail();
|
|
});
|
|
|
|
// -- Add Token --
|
|
$("btn-add-token-confirm").addEventListener("click", () => {
|
|
const contractAddr = $("add-token-address").value.trim();
|
|
if (!contractAddr || !contractAddr.startsWith("0x")) {
|
|
showError(
|
|
"add-token-error",
|
|
"Please enter a valid contract address starting with 0x.",
|
|
);
|
|
return;
|
|
}
|
|
hideError("add-token-error");
|
|
// TODO: look up token name/symbol/decimals from contract via background
|
|
const addr = currentAddress();
|
|
if (addr) {
|
|
addr.tokens.push({
|
|
contractAddress: contractAddr,
|
|
symbol: "TKN",
|
|
decimals: 18,
|
|
balance: "0",
|
|
});
|
|
}
|
|
showAddressDetail();
|
|
});
|
|
|
|
$("btn-add-token-back").addEventListener("click", () => {
|
|
showAddressDetail();
|
|
});
|
|
|
|
// -- Settings --
|
|
$("btn-save-rpc").addEventListener("click", () => {
|
|
state.rpcUrl = $("settings-rpc").value.trim();
|
|
// TODO: persist via background
|
|
});
|
|
|
|
$("btn-settings-back").addEventListener("click", () => {
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
// -- Approval --
|
|
$("btn-approve").addEventListener("click", () => {
|
|
// TODO: send approval to background
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
$("btn-reject").addEventListener("click", () => {
|
|
// TODO: send rejection to background
|
|
renderWalletList();
|
|
showView("main");
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|