Redesign UI for non-technical users
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.
This commit is contained in:
2026-02-24 10:21:52 +07:00
parent e41efc969d
commit 8431488849
3 changed files with 652 additions and 335 deletions

123
README.md
View File

@@ -72,7 +72,11 @@ manifest/
### UI Design Philosophy
The UI follows a "Universal Paperclips" aesthetic — a deliberately spartan,
almost brutalist approach:
almost brutalist approach. The guiding principle is that an unskilled,
non-technical person should be able to figure out how to use it without any
crypto knowledge.
#### Visual Style
- **Monochrome**: Black text on white background. No brand colors, no gradients,
no color-coding. Color may be introduced later for specific semantic purposes
@@ -84,9 +88,6 @@ almost brutalist approach:
Ethereum addresses, transaction hashes, and balances are inherently
fixed-width data. Rather than mixing proportional and monospace fonts, we use
monospace everywhere for visual consistency and alignment.
- **Icon fonts only**: Where icons are needed (copy, send, settings, etc.) we
use an icon font (no SVGs, no PNGs, no image assets). This keeps the extension
size tiny and the build simple.
- **No images**: Zero image assets in the entire extension. No logos, no
illustrations, no token icons. Token identity is conveyed by symbol text (ETH,
USDC, etc.).
@@ -100,28 +101,76 @@ almost brutalist approach:
- **360x600 popup**: Standard browser extension popup dimensions. The UI is
designed for this fixed viewport — no responsive breakpoints needed.
#### Language & Labeling
All user-facing text avoids crypto jargon wherever possible:
- "Recovery phrase" instead of "seed phrase", "mnemonic", or "BIP-39 mnemonic"
- "Address" instead of "account", "derived key", or "HD child"
- "Password" instead of "encryption key" or "vault passphrase"
- "Private key" instead of "secret key" or "signing key"
- Buttons use plain verbs: "Send", "Receive", "Copy address", "Add", "Back",
"Cancel", "Lock", "Unlock", "Allow", "Deny"
- No bracket notation like `[locked]` or `[setup]` — just plain titles
- Helpful inline descriptions where needed (e.g. "This password locks the wallet
on this device. It is not the same as your recovery phrase.")
- Error messages are full sentences ("Please enter your password." not "password
required")
#### Data Model
The core hierarchy is **Wallets → Addresses**:
- A **wallet** is either:
- An **HD wallet** (recovery phrase): generates multiple addresses from a
single 12/24 word recovery phrase using BIP-39/BIP-44 derivation. The user
can add more addresses with a "+" button.
- A **key wallet** (private key): a single address imported directly from a
private key. No "+" button since there is only one address.
- An **address** holds ETH and any user-added ERC-20 tokens.
- The user can have multiple wallets, each with multiple addresses (HD) or a
single address (key).
#### Navigation
The main view shows all addresses grouped by wallet, with ETH balances inline.
The user taps an address to see its detail view (full address, balance, tokens,
send/receive). Navigation is flat — every view has a "Back" or "Cancel" button
that returns to the previous context. No deep nesting, no tabs, no hamburger
menus.
### UI Views
The popup has the following views, switched via simple show/hide:
1. **Lock screen**: Password input + unlock button. Shown when the wallet is
locked or on first open after browser restart.
2. **Setup / Onboarding**: Shown on first use. Create a new wallet (generates
BIP-39 mnemonic, user confirms backup, sets password) or import an existing
seed phrase.
3. **Main / Account view**: The default view after unlock. Shows the current
account address (truncated, click to copy), ETH balance, list of added ERC-20
tokens with balances, and action buttons (Send, Receive, Settings).
4. **Send**: Form with To address, amount, token selector (ETH or any added
ERC-20), and a Send button. Shows gas estimate before confirming.
5. **Receive**: Displays the current account address in full (copyable). That's
it.
6. **Settings**: Configure RPC endpoint URL, manage wallets (add/remove seed
phrases), derive additional accounts, view/export seed phrase (requires
password re-entry), manage added ERC-20 tokens.
7. **Approval popups**: When a connected site requests a transaction signature
or message signature, an approval view shows the details and Approve/Reject
buttons.
1. **Lock**: Password input + Unlock button. Shown when the wallet is locked or
on first open after browser restart.
2. **Welcome**: Shown on first use. Three options: "Create a new wallet", "I
have a recovery phrase", "I have a private key". Password is set during this
first flow.
3. **Create**: Displays a generated 12-word recovery phrase with instructions to
write it down. User sets a password and confirms.
4. **Import recovery phrase**: Paste a 12 or 24 word recovery phrase. Password
fields shown only on first use.
5. **Import private key**: Paste a private key. Password fields shown only on
first use.
6. **Main**: All wallets listed, each showing its addresses with truncated
address and ETH balance. "+" next to HD wallets to add another address. "+
Add wallet" at the bottom. Settings and Lock buttons in the header. Future: a
sub-heading showing total portfolio value in USD (and eventually other
currencies).
7. **Add wallet**: Choose wallet type (new, recovery phrase, private key) — same
three options as Welcome but without password setup.
8. **Address detail**: Full address (click to copy), ETH balance, USD value
(future), Send/Receive buttons, token list with "+ Add" button.
9. **Send**: Token selector, recipient address, amount. Cancel returns to
address detail.
10. **Receive**: Full address displayed with "Copy address" button.
11. **Add token**: Enter contract address. The extension looks up the token
name/symbol automatically.
12. **Settings**: Network (RPC endpoint URL) with explanatory text.
13. **Approval**: When a website requests wallet access or a signature, shows
the site origin, request details, and Allow/Deny buttons.
### External Services
@@ -167,30 +216,31 @@ want maximum privacy can point it at their own Ethereum node.
### Supported Functionality
- Create new wallet from generated BIP-39 mnemonic
- Import wallet from existing BIP-39 mnemonic
- Derive multiple HD addresses per wallet (`m/44'/60'/0'/0/n`)
- View ETH balance
- Create new HD wallet (generates 12-word recovery phrase)
- Import HD wallet from existing 12 or 24 word recovery phrase
- Import single-address wallet from private key
- Add multiple addresses within an HD wallet
- Manage multiple wallets simultaneously
- View ETH balance per address
- View ERC-20 token balances (user adds token by contract address)
- Send ETH to an address
- Send ERC-20 tokens to an address
- Receive ETH/tokens (display address + copy to clipboard)
- Receive ETH/tokens (display address, copy to clipboard)
- Connect to web3 sites (EIP-1193 `eth_requestAccounts`)
- Sign transactions requested by connected sites
- Sign messages (`personal_sign`, `eth_sign`)
- Switch between wallets/accounts
- Lock/unlock with password
- Configurable RPC endpoint
- Future: USD value display (and other fiat currencies)
### Non-Goals
- Token swaps (use a DEX in the browser)
- Portfolio/price tracking (balances shown in token units only, no fiat)
- NFT display or management
- Multi-chain support (Ethereum mainnet only, for now)
- Analytics, telemetry, or tracking of any kind
- Advertisements or promotions
- Phishing detection (use your brain)
- Phishing detection
- Hardware wallet support (maybe later)
- Token list auto-discovery (user adds tokens manually)
- Fiat on/off ramps
@@ -200,19 +250,22 @@ want maximum privacy can point it at their own Ethereum node.
## TODO
- [x] Project scaffolding (Makefile, Dockerfile, CI, manifests)
- [ ] Set up Tailwind CSS build pipeline
- [ ] Build popup UI views (lock, setup, main, send, receive, settings)
- [x] Set up Tailwind CSS build pipeline
- [x] Build popup UI views (all 13 views with stub state)
- [ ] Implement BIP-39 mnemonic generation and validation
- [ ] Implement BIP-32/BIP-44 HD key derivation for Ethereum
- [ ] Implement encrypted storage for seed phrases
- [ ] Implement private key import and address derivation
- [ ] Implement encrypted storage for wallet data
- [ ] Implement background wallet manager
- [ ] Implement EIP-1193 provider and content script injection
- [ ] Implement ETH balance lookup and send
- [ ] Implement ETH balance lookup via RPC
- [ ] Implement ETH send (transaction construction + signing)
- [ ] Implement ERC-20 token management (add by contract, view balance, send)
- [ ] Implement site connection approval flow
- [ ] Implement transaction signing approval flow
- [ ] Implement message signing (`personal_sign`, `eth_sign`)
- [ ] Add configurable RPC endpoint
- [ ] Add USD value display (price feed TBD)
- [ ] Add multi-currency fiat display support
- [ ] Test on Chrome and Firefox
- [ ] Write tests for crypto operations
- [ ] Write tests for transaction construction

View File

@@ -11,22 +11,24 @@
<!-- ============ LOCK SCREEN ============ -->
<div id="view-lock" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [locked]
AutistMask
</h1>
<p class="mb-3">
Your wallet is locked. Enter your password to continue.
</p>
<div class="mb-2">
<label class="block mb-1">Password:</label>
<label class="block mb-1">Password</label>
<input
type="password"
id="unlock-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="enter password to unlock"
/>
</div>
<button
id="btn-unlock"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
unlock
Unlock
</button>
<div
id="unlock-error"
@@ -34,57 +36,70 @@
></div>
</div>
<!-- ============ SETUP: WELCOME ============ -->
<div id="view-setup" class="view hidden">
<!-- ============ WELCOME / FIRST USE ============ -->
<div id="view-welcome" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [setup]
Welcome to AutistMask
</h1>
<p class="mb-3">No wallet found. Create or import one.</p>
<div class="flex gap-2">
<p class="mb-3">
To get started, add a wallet. You can create a brand new one
or bring in one you already have.
</p>
<div class="flex flex-col gap-2">
<button
id="btn-setup-create"
id="btn-welcome-new"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
create new wallet
Create a new wallet
</button>
<button
id="btn-setup-import"
id="btn-welcome-recovery"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import seed phrase
I have a recovery phrase
</button>
<button
id="btn-welcome-key"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
I have a private key
</button>
</div>
</div>
<!-- ============ SETUP: CREATE ============ -->
<!-- ============ CREATE NEW WALLET ============ -->
<div id="view-create" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [create wallet]
Create New Wallet
</h1>
<p class="mb-2">
Write down this seed phrase and store it safely. It is the
only way to recover your wallet.
These 12 words are your recovery phrase. Write them down on
paper and keep them somewhere safe. Anyone with these words
can access your funds. If you lose them, your wallet cannot
be recovered.
</p>
<div
id="create-mnemonic"
class="border border-border p-2 mb-3 font-mono break-all select-all"
class="border border-border p-2 mb-3 break-all select-all leading-relaxed"
></div>
<div class="mb-2">
<label class="block mb-1">Set password:</label>
<label class="block mb-1">Choose a password</label>
<p class="text-xs text-muted mb-1">
This password locks the wallet on this device. It is not
the same as your recovery phrase.
</p>
<input
type="password"
id="create-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/>
</div>
<div class="mb-2">
<label class="block mb-1">Confirm password:</label>
<label class="block mb-1">Confirm password</label>
<input
type="password"
id="create-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/>
</div>
<div class="flex gap-2">
@@ -92,13 +107,13 @@
id="btn-create-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
i backed it up, create wallet
I wrote it down — continue
</button>
<button
id="btn-create-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Back
</button>
</div>
<div
@@ -107,61 +122,119 @@
></div>
</div>
<!-- ============ SETUP: IMPORT ============ -->
<div id="view-import" class="view hidden">
<!-- ============ IMPORT RECOVERY PHRASE ============ -->
<div id="view-import-phrase" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [import wallet]
Import Recovery Phrase
</h1>
<p class="mb-2">
Enter the 12 or 24 word recovery phrase from your existing
wallet. Separate each word with a space.
</p>
<div class="mb-2">
<label class="block mb-1"
>Seed phrase (12 or 24 words):</label
>
<textarea
id="import-mnemonic"
rows="3"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg resize-y"
placeholder="word1 word2 word3 ..."
placeholder="word word word ..."
></textarea>
</div>
<div class="mb-2">
<label class="block mb-1">Set password:</label>
<div class="mb-2" id="import-phrase-password-section">
<label class="block mb-1">Choose a password</label>
<p class="text-xs text-muted mb-1">
This password locks the wallet on this device.
</p>
<input
type="password"
id="import-password"
id="import-phrase-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/>
</div>
<div class="mb-2">
<label class="block mb-1">Confirm password:</label>
<div class="mb-2" id="import-phrase-password-confirm-section">
<label class="block mb-1">Confirm password</label>
<input
type="password"
id="import-password-confirm"
id="import-phrase-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/>
</div>
<div class="flex gap-2">
<button
id="btn-import-confirm"
id="btn-import-phrase-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import wallet
Import
</button>
<button
id="btn-import-back"
id="btn-import-phrase-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Back
</button>
</div>
<div
id="import-error"
id="import-phrase-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ MAIN / ACCOUNT VIEW ============ -->
<!-- ============ IMPORT PRIVATE KEY ============ -->
<div id="view-import-key" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
Import Private Key
</h1>
<p class="mb-2">
Paste your private key below. This wallet will have a single
address.
</p>
<div class="mb-2">
<input
type="password"
id="import-private-key"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0x..."
/>
</div>
<div class="mb-2" id="import-key-password-section">
<label class="block mb-1">Choose a password</label>
<p class="text-xs text-muted mb-1">
This password locks the wallet on this device.
</p>
<input
type="password"
id="import-key-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/>
</div>
<div class="mb-2" id="import-key-password-confirm-section">
<label class="block mb-1">Confirm password</label>
<input
type="password"
id="import-key-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
/>
</div>
<div class="flex gap-2">
<button
id="btn-import-key-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Import
</button>
<button
id="btn-import-key-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Back
</button>
</div>
<div
id="import-key-error"
class="mt-2 border border-border border-dashed p-1 hidden"
></div>
</div>
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
<div id="view-main" class="view hidden">
<div
class="flex justify-between items-center border-b border-border pb-1 mb-2"
@@ -171,61 +244,128 @@
<button
id="btn-settings"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="settings"
title="Settings"
>
[=]
Settings
</button>
<button
id="btn-lock"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="lock"
title="Lock wallet"
>
[x]
Lock
</button>
</div>
</div>
<!-- account selector -->
<div class="mb-2">
<div class="flex justify-between items-center">
<select
id="account-selector"
class="border border-border p-1 font-mono text-sm bg-bg text-fg flex-1 mr-2"
></select>
<button
id="btn-copy-address"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
title="copy address"
>
[cp]
</button>
</div>
<div
id="current-address"
class="text-xs text-muted mt-1 break-all"
></div>
<!-- wallet list -->
<div id="wallet-list"></div>
<!-- add wallet button -->
<div class="mt-3 border-t border-border pt-2">
<button
id="btn-add-wallet"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
+ Add wallet
</button>
</div>
</div>
<!-- ============ ADD WALLET (from main view) ============ -->
<div id="view-add-wallet" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
Add Wallet
</h1>
<p class="mb-3">What kind of wallet do you want to add?</p>
<div class="flex flex-col gap-2">
<button
id="btn-add-wallet-new"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-left"
>
Create a new wallet
<span class="block text-xs text-muted"
>Generates a new recovery phrase with one
address</span
>
</button>
<button
id="btn-add-wallet-phrase"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-left"
>
Import recovery phrase
<span class="block text-xs text-muted"
>Use an existing 12 or 24 word recovery phrase</span
>
</button>
<button
id="btn-add-wallet-key"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-left"
>
Import private key
<span class="block text-xs text-muted"
>A single address from a private key</span
>
</button>
</div>
<div class="mt-3">
<button
id="btn-add-wallet-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Back
</button>
</div>
</div>
<!-- ============ ADDRESS DETAIL VIEW ============ -->
<div id="view-address" class="view hidden">
<div
class="flex justify-between items-center border-b border-border pb-1 mb-2"
>
<h1 class="font-bold" id="address-title">Address</h1>
<button
id="btn-address-back"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Back
</button>
</div>
<div
id="address-full"
class="text-xs break-all mb-1 cursor-pointer"
title="Click to copy"
></div>
<div
class="text-xs text-muted mb-3"
id="address-copied-msg"
></div>
<!-- balance -->
<div class="border-b border-border-light pb-2 mb-2">
<div class="text-base font-bold">
<span id="eth-balance">0.0000</span> ETH
<span id="address-eth-balance">0.0000</span> ETH
</div>
<div
class="text-xs text-muted"
id="address-usd-value"
></div>
</div>
<!-- action buttons -->
<!-- actions -->
<div class="flex gap-2 mb-3">
<button
id="btn-send"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
>
send
Send
</button>
<button
id="btn-receive"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
>
receive
Receive
</button>
</div>
@@ -239,24 +379,22 @@
id="btn-add-token"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
>
[+]
+ Add
</button>
</div>
<div id="token-list">
<div class="text-muted text-xs py-1">
no tokens added
No tokens added yet. Use "+ Add" to track a token.
</div>
</div>
</div>
</div>
<!-- ============ SEND VIEW ============ -->
<!-- ============ SEND ============ -->
<div id="view-send" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [send]
</h1>
<h1 class="font-bold border-b border-border pb-1 mb-3">Send</h1>
<div class="mb-2">
<label class="block mb-1">Token:</label>
<label class="block mb-1">What to send</label>
<select
id="send-token"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
@@ -265,16 +403,16 @@
</select>
</div>
<div class="mb-2">
<label class="block mb-1">To address:</label>
<label class="block mb-1">To</label>
<input
type="text"
id="send-to"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0x..."
placeholder="Recipient address (0x...)"
/>
</div>
<div class="mb-2">
<label class="block mb-1">Amount:</label>
<label class="block mb-1">Amount</label>
<input
type="text"
id="send-amount"
@@ -283,7 +421,7 @@
/>
</div>
<div
id="send-gas-estimate"
id="send-fee-estimate"
class="text-xs text-muted mb-2 hidden"
></div>
<div class="flex gap-2">
@@ -291,13 +429,13 @@
id="btn-send-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
send
Send
</button>
<button
id="btn-send-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Cancel
</button>
</div>
<div
@@ -306,39 +444,46 @@
></div>
</div>
<!-- ============ RECEIVE VIEW ============ -->
<!-- ============ RECEIVE ============ -->
<div id="view-receive" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [receive]
Receive
</h1>
<p class="mb-2">Your address:</p>
<p class="mb-2">
Share this address with the sender. Make sure you only use
this address to receive Ethereum tokens.
</p>
<div
id="receive-address"
class="border border-border p-2 font-mono break-all select-all mb-3"
class="border border-border p-2 break-all select-all mb-3"
></div>
<div class="flex gap-2">
<button
id="btn-receive-copy"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
copy to clipboard
Copy address
</button>
<button
id="btn-receive-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Back
</button>
</div>
</div>
<!-- ============ ADD TOKEN VIEW ============ -->
<!-- ============ ADD TOKEN ============ -->
<div id="view-add-token" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [add token]
Add Token
</h1>
<p class="mb-2">
Enter the contract address of the token you want to track.
You can find this on the token's page on Etherscan.
</p>
<div class="mb-2">
<label class="block mb-1">Token contract address:</label>
<label class="block mb-1">Contract address</label>
<input
type="text"
id="add-token-address"
@@ -355,13 +500,13 @@
id="btn-add-token-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
add token
Add
</button>
<button
id="btn-add-token-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Cancel
</button>
</div>
<div
@@ -370,81 +515,50 @@
></div>
</div>
<!-- ============ SETTINGS VIEW ============ -->
<!-- ============ SETTINGS ============ -->
<div id="view-settings" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [settings]
Settings
</h1>
<h2 class="font-bold mb-1">RPC Endpoint</h2>
<h2 class="font-bold mb-1">Network</h2>
<p class="text-xs text-muted mb-1">
The server used to talk to the Ethereum network. Change this
if you run your own node or prefer a different provider.
</p>
<div class="mb-3">
<input
type="text"
id="settings-rpc"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="https://..."
/>
<button
id="btn-save-rpc"
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
>
save
Save
</button>
</div>
<h2 class="font-bold mb-1">Accounts</h2>
<div class="mb-3">
<button
id="btn-derive-account"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
derive next account
</button>
</div>
<h2 class="font-bold mb-1">Wallets</h2>
<div class="mb-3">
<button
id="btn-show-seed"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
show seed phrase
</button>
<button
id="btn-import-additional"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
import another wallet
</button>
</div>
<div
id="settings-seed-display"
class="border border-border p-2 font-mono break-all mb-3 hidden"
></div>
<h2 class="font-bold mb-1">Tokens</h2>
<div id="settings-token-list" class="mb-3">
<div class="text-muted text-xs">no tokens added</div>
</div>
<div class="border-t border-border pt-2">
<button
id="btn-settings-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
back
Back
</button>
</div>
</div>
<!-- ============ APPROVAL VIEW ============ -->
<!-- ============ APPROVAL ============ -->
<div id="view-approve" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [approve request]
A website is requesting access
</h1>
<div class="mb-2">
<div class="text-xs text-muted mb-1">
Site: <span id="approve-origin"></span>
From:
<span id="approve-origin" class="font-bold"></span>
</div>
<div class="font-bold mb-1" id="approve-type"></div>
</div>
@@ -457,13 +571,13 @@
id="btn-approve"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
approve
Allow
</button>
<button
id="btn-reject"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
reject
Deny
</button>
</div>
</div>

View File

@@ -1,12 +1,15 @@
// AutistMask popup UI — view management and event wiring
// All wallet logic will live in background/; this file is purely UI.
const views = [
const VIEWS = [
"lock",
"setup",
"welcome",
"create",
"import",
"import-phrase",
"import-key",
"main",
"add-wallet",
"address",
"send",
"receive",
"add-token",
@@ -15,7 +18,7 @@ const views = [
];
function showView(name) {
for (const v of views) {
for (const v of VIEWS) {
const el = document.getElementById(`view-${v}`);
if (el) {
el.classList.toggle("hidden", v !== name);
@@ -24,15 +27,18 @@ function showView(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,
accounts: [],
selectedAccount: 0,
tokens: [],
wallets: [],
selectedWallet: null,
selectedAddress: null,
rpcUrl: "https://eth.llamarpc.com",
mnemonic: null,
isFirstSetup: true,
};
// -- helpers --
@@ -52,63 +58,190 @@ function hideError(id) {
function truncateAddress(addr) {
if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4);
return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
}
function updateAccountSelector() {
const sel = $("account-selector");
sel.innerHTML = "";
state.accounts.forEach((acct, i) => {
const opt = document.createElement("option");
opt.value = i;
opt.textContent = `Account ${i}: ${truncateAddress(acct.address)}`;
sel.appendChild(opt);
});
sel.value = state.selectedAccount;
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
function makeStubAddress() {
const hex = Array.from({ length: 40 }, () =>
Math.floor(Math.random() * 16).toString(16),
).join("");
return {
address: "0x" + hex,
balance: "0.0000",
tokens: [],
};
}
function updateTokenList() {
const list = $("token-list");
if (state.tokens.length === 0) {
list.innerHTML =
'<div class="text-muted text-xs py-1">no tokens added</div>';
// -- 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;
}
list.innerHTML = state.tokens
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.0000"}</span>` +
`<span>${t.balance || "0"}</span>` +
`</div>`,
)
.join("");
}
function updateSendTokenSelect() {
function updateSendTokenSelect(addr) {
const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>';
state.tokens.forEach((t) => {
addr.tokens.forEach((t) => {
const opt = document.createElement("option");
opt.value = t.address;
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() {
// For now, always show setup (no wallet exists yet).
// Once background messaging is wired, this will check actual state.
if (!state.hasWallet) {
showView("setup");
showView("welcome");
} else if (state.locked) {
showView("lock");
} else {
renderWalletList();
showView("main");
}
@@ -116,100 +249,119 @@ function init() {
$("btn-unlock").addEventListener("click", () => {
const pw = $("unlock-password").value;
if (!pw) {
showError("unlock-error", "enter a password");
showError("unlock-error", "Please enter your password.");
return;
}
hideError("unlock-error");
// TODO: send unlock message to background
state.locked = false;
updateAccountSelector();
updateTokenList();
renderWalletList();
showView("main");
});
// -- Setup --
$("btn-setup-create").addEventListener("click", () => {
// TODO: request mnemonic generation from background
$("create-mnemonic").textContent =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
showView("create");
});
$("btn-setup-import").addEventListener("click", () => {
showView("import");
});
// -- 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", () => {
const pw = $("create-password").value;
const pw2 = $("create-password-confirm").value;
if (!pw) {
showError("create-error", "enter a password");
return;
}
if (pw !== pw2) {
showError("create-error", "passwords do not match");
if (
!validatePasswords(
"create-password",
"create-password-confirm",
"create-error",
)
) {
return;
}
hideError("create-error");
// TODO: send create wallet message to background
state.hasWallet = true;
state.locked = false;
state.password = pw;
state.mnemonic = $("create-mnemonic").textContent;
state.accounts = [
{
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
const walletNum = state.wallets.length + 1;
addWalletAndGoToMain({
type: "hd",
name: "Wallet " + walletNum,
mnemonic: $("create-mnemonic").textContent,
addresses: [makeStubAddress()],
});
});
$("btn-create-back").addEventListener("click", () => {
showView("setup");
showView(state.isFirstSetup ? "welcome" : "add-wallet");
});
// -- Import wallet --
$("btn-import-confirm").addEventListener("click", () => {
// -- Import recovery phrase --
$("btn-import-phrase-confirm").addEventListener("click", () => {
const mnemonic = $("import-mnemonic").value.trim();
const pw = $("import-password").value;
const pw2 = $("import-password-confirm").value;
if (!mnemonic) {
showError("import-error", "enter a seed phrase");
showError(
"import-phrase-error",
"Please enter your recovery phrase.",
);
return;
}
if (!pw) {
showError("import-error", "enter a password");
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 (pw !== pw2) {
showError("import-error", "passwords do not match");
if (
!validatePasswords(
"import-phrase-password",
"import-phrase-password-confirm",
"import-phrase-error",
)
) {
return;
}
hideError("import-error");
// TODO: validate mnemonic and send to background
state.hasWallet = true;
state.locked = false;
state.password = pw;
state.mnemonic = mnemonic;
state.accounts = [
{
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
hideError("import-phrase-error");
const walletNum = state.wallets.length + 1;
addWalletAndGoToMain({
type: "hd",
name: "Wallet " + walletNum,
mnemonic: mnemonic,
addresses: [makeStubAddress()],
});
});
$("btn-import-back").addEventListener("click", () => {
showView("setup");
$("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 --
@@ -224,33 +376,50 @@ function init() {
showView("settings");
});
$("account-selector").addEventListener("change", (e) => {
state.selectedAccount = parseInt(e.target.value, 10);
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
$("btn-add-wallet").addEventListener("click", () => {
showView("add-wallet");
});
$("btn-copy-address").addEventListener("click", () => {
const addr = state.accounts[state.selectedAccount]?.address;
// -- 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", () => {
updateSendTokenSelect();
$("send-to").value = "";
$("send-amount").value = "";
$("send-gas-estimate").classList.add("hidden");
$("send-fee-estimate").classList.add("hidden");
$("send-status").classList.add("hidden");
showView("send");
});
$("btn-receive").addEventListener("click", () => {
$("receive-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
const addr = currentAddress();
$("receive-address").textContent = addr ? addr.address : "";
showView("receive");
});
@@ -266,23 +435,23 @@ function init() {
const to = $("send-to").value.trim();
const amount = $("send-amount").value.trim();
if (!to) {
showError("send-status", "enter a recipient address");
showError("send-status", "Please enter a recipient address.");
$("send-status").classList.remove("hidden");
return;
}
if (!amount || isNaN(parseFloat(amount))) {
showError("send-status", "enter a valid amount");
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 = "transaction sent (stub)";
el.textContent = "Sent! (stub)";
el.classList.remove("hidden");
});
$("btn-send-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Receive --
@@ -294,30 +463,35 @@ function init() {
});
$("btn-receive-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Add Token --
$("btn-add-token-confirm").addEventListener("click", () => {
const addr = $("add-token-address").value.trim();
if (!addr || !addr.startsWith("0x")) {
showError("add-token-error", "enter a valid contract address");
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
state.tokens.push({
address: addr,
symbol: "TKN",
decimals: 18,
balance: "0.0000",
});
updateTokenList();
showView("main");
const addr = currentAddress();
if (addr) {
addr.tokens.push({
contractAddress: contractAddr,
symbol: "TKN",
decimals: 18,
balance: "0",
});
}
showAddressDetail();
});
$("btn-add-token-back").addEventListener("click", () => {
showView("main");
showAddressDetail();
});
// -- Settings --
@@ -326,45 +500,21 @@ function init() {
// TODO: persist via background
});
$("btn-derive-account").addEventListener("click", () => {
const idx = state.accounts.length;
// TODO: derive from seed via background
state.accounts.push({
address: `0x${idx.toString(16).padStart(40, "0")}`,
balance: "0.0000",
});
updateAccountSelector();
});
$("btn-show-seed").addEventListener("click", () => {
const display = $("settings-seed-display");
if (display.classList.contains("hidden")) {
// TODO: require password re-entry, get from background
display.textContent = state.mnemonic || "(no seed loaded)";
display.classList.remove("hidden");
} else {
display.classList.add("hidden");
}
});
$("btn-import-additional").addEventListener("click", () => {
showView("import");
});
$("btn-settings-back").addEventListener("click", () => {
updateAccountSelector();
updateTokenList();
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");
});
}