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 ### UI Design Philosophy
The UI follows a "Universal Paperclips" aesthetic — a deliberately spartan, 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, - **Monochrome**: Black text on white background. No brand colors, no gradients,
no color-coding. Color may be introduced later for specific semantic purposes 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 Ethereum addresses, transaction hashes, and balances are inherently
fixed-width data. Rather than mixing proportional and monospace fonts, we use fixed-width data. Rather than mixing proportional and monospace fonts, we use
monospace everywhere for visual consistency and alignment. 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 - **No images**: Zero image assets in the entire extension. No logos, no
illustrations, no token icons. Token identity is conveyed by symbol text (ETH, illustrations, no token icons. Token identity is conveyed by symbol text (ETH,
USDC, etc.). USDC, etc.).
@@ -100,28 +101,76 @@ almost brutalist approach:
- **360x600 popup**: Standard browser extension popup dimensions. The UI is - **360x600 popup**: Standard browser extension popup dimensions. The UI is
designed for this fixed viewport — no responsive breakpoints needed. 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 ### UI Views
The popup has the following views, switched via simple show/hide: The popup has the following views, switched via simple show/hide:
1. **Lock screen**: Password input + unlock button. Shown when the wallet is 1. **Lock**: Password input + Unlock button. Shown when the wallet is locked or
locked or on first open after browser restart. on first open after browser restart.
2. **Setup / Onboarding**: Shown on first use. Create a new wallet (generates 2. **Welcome**: Shown on first use. Three options: "Create a new wallet", "I
BIP-39 mnemonic, user confirms backup, sets password) or import an existing have a recovery phrase", "I have a private key". Password is set during this
seed phrase. first flow.
3. **Main / Account view**: The default view after unlock. Shows the current 3. **Create**: Displays a generated 12-word recovery phrase with instructions to
account address (truncated, click to copy), ETH balance, list of added ERC-20 write it down. User sets a password and confirms.
tokens with balances, and action buttons (Send, Receive, Settings). 4. **Import recovery phrase**: Paste a 12 or 24 word recovery phrase. Password
4. **Send**: Form with To address, amount, token selector (ETH or any added fields shown only on first use.
ERC-20), and a Send button. Shows gas estimate before confirming. 5. **Import private key**: Paste a private key. Password fields shown only on
5. **Receive**: Displays the current account address in full (copyable). That's first use.
it. 6. **Main**: All wallets listed, each showing its addresses with truncated
6. **Settings**: Configure RPC endpoint URL, manage wallets (add/remove seed address and ETH balance. "+" next to HD wallets to add another address. "+
phrases), derive additional accounts, view/export seed phrase (requires Add wallet" at the bottom. Settings and Lock buttons in the header. Future: a
password re-entry), manage added ERC-20 tokens. sub-heading showing total portfolio value in USD (and eventually other
7. **Approval popups**: When a connected site requests a transaction signature currencies).
or message signature, an approval view shows the details and Approve/Reject 7. **Add wallet**: Choose wallet type (new, recovery phrase, private key) — same
buttons. 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 ### External Services
@@ -167,30 +216,31 @@ want maximum privacy can point it at their own Ethereum node.
### Supported Functionality ### Supported Functionality
- Create new wallet from generated BIP-39 mnemonic - Create new HD wallet (generates 12-word recovery phrase)
- Import wallet from existing BIP-39 mnemonic - Import HD wallet from existing 12 or 24 word recovery phrase
- Derive multiple HD addresses per wallet (`m/44'/60'/0'/0/n`) - Import single-address wallet from private key
- View ETH balance - 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) - View ERC-20 token balances (user adds token by contract address)
- Send ETH to an address - Send ETH to an address
- Send ERC-20 tokens 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`) - Connect to web3 sites (EIP-1193 `eth_requestAccounts`)
- Sign transactions requested by connected sites - Sign transactions requested by connected sites
- Sign messages (`personal_sign`, `eth_sign`) - Sign messages (`personal_sign`, `eth_sign`)
- Switch between wallets/accounts
- Lock/unlock with password - Lock/unlock with password
- Configurable RPC endpoint - Configurable RPC endpoint
- Future: USD value display (and other fiat currencies)
### Non-Goals ### Non-Goals
- Token swaps (use a DEX in the browser) - Token swaps (use a DEX in the browser)
- Portfolio/price tracking (balances shown in token units only, no fiat)
- NFT display or management - NFT display or management
- Multi-chain support (Ethereum mainnet only, for now) - Multi-chain support (Ethereum mainnet only, for now)
- Analytics, telemetry, or tracking of any kind - Analytics, telemetry, or tracking of any kind
- Advertisements or promotions - Advertisements or promotions
- Phishing detection (use your brain) - Phishing detection
- Hardware wallet support (maybe later) - Hardware wallet support (maybe later)
- Token list auto-discovery (user adds tokens manually) - Token list auto-discovery (user adds tokens manually)
- Fiat on/off ramps - Fiat on/off ramps
@@ -200,19 +250,22 @@ want maximum privacy can point it at their own Ethereum node.
## TODO ## TODO
- [x] Project scaffolding (Makefile, Dockerfile, CI, manifests) - [x] Project scaffolding (Makefile, Dockerfile, CI, manifests)
- [ ] Set up Tailwind CSS build pipeline - [x] Set up Tailwind CSS build pipeline
- [ ] Build popup UI views (lock, setup, main, send, receive, settings) - [x] Build popup UI views (all 13 views with stub state)
- [ ] Implement BIP-39 mnemonic generation and validation - [ ] Implement BIP-39 mnemonic generation and validation
- [ ] Implement BIP-32/BIP-44 HD key derivation for Ethereum - [ ] 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 background wallet manager
- [ ] Implement EIP-1193 provider and content script injection - [ ] 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 ERC-20 token management (add by contract, view balance, send)
- [ ] Implement site connection approval flow - [ ] Implement site connection approval flow
- [ ] Implement transaction signing approval flow - [ ] Implement transaction signing approval flow
- [ ] Implement message signing (`personal_sign`, `eth_sign`) - [ ] 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 - [ ] Test on Chrome and Firefox
- [ ] Write tests for crypto operations - [ ] Write tests for crypto operations
- [ ] Write tests for transaction construction - [ ] Write tests for transaction construction

View File

@@ -11,22 +11,24 @@
<!-- ============ LOCK SCREEN ============ --> <!-- ============ LOCK SCREEN ============ -->
<div id="view-lock" class="view hidden"> <div id="view-lock" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [locked] AutistMask
</h1> </h1>
<p class="mb-3">
Your wallet is locked. Enter your password to continue.
</p>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Password:</label> <label class="block mb-1">Password</label>
<input <input
type="password" type="password"
id="unlock-password" id="unlock-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="enter password to unlock"
/> />
</div> </div>
<button <button
id="btn-unlock" id="btn-unlock"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
unlock Unlock
</button> </button>
<div <div
id="unlock-error" id="unlock-error"
@@ -34,57 +36,70 @@
></div> ></div>
</div> </div>
<!-- ============ SETUP: WELCOME ============ --> <!-- ============ WELCOME / FIRST USE ============ -->
<div id="view-setup" class="view hidden"> <div id="view-welcome" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [setup] Welcome to AutistMask
</h1> </h1>
<p class="mb-3">No wallet found. Create or import one.</p> <p class="mb-3">
<div class="flex gap-2"> 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 <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" 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>
<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" 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> </button>
</div> </div>
</div> </div>
<!-- ============ SETUP: CREATE ============ --> <!-- ============ CREATE NEW WALLET ============ -->
<div id="view-create" class="view hidden"> <div id="view-create" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [create wallet] Create New Wallet
</h1> </h1>
<p class="mb-2"> <p class="mb-2">
Write down this seed phrase and store it safely. It is the These 12 words are your recovery phrase. Write them down on
only way to recover your wallet. paper and keep them somewhere safe. Anyone with these words
can access your funds. If you lose them, your wallet cannot
be recovered.
</p> </p>
<div <div
id="create-mnemonic" 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>
<div class="mb-2"> <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 <input
type="password" type="password"
id="create-password" id="create-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/> />
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Confirm password:</label> <label class="block mb-1">Confirm password</label>
<input <input
type="password" type="password"
id="create-password-confirm" id="create-password-confirm"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/> />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -92,13 +107,13 @@
id="btn-create-confirm" id="btn-create-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" 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>
<button <button
id="btn-create-back" id="btn-create-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Back
</button> </button>
</div> </div>
<div <div
@@ -107,61 +122,119 @@
></div> ></div>
</div> </div>
<!-- ============ SETUP: IMPORT ============ --> <!-- ============ IMPORT RECOVERY PHRASE ============ -->
<div id="view-import" class="view hidden"> <div id="view-import-phrase" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [import wallet] Import Recovery Phrase
</h1> </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"> <div class="mb-2">
<label class="block mb-1"
>Seed phrase (12 or 24 words):</label
>
<textarea <textarea
id="import-mnemonic" id="import-mnemonic"
rows="3" rows="3"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg resize-y" 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> ></textarea>
</div> </div>
<div class="mb-2"> <div class="mb-2" id="import-phrase-password-section">
<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.
</p>
<input <input
type="password" 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" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="password"
/> />
</div> </div>
<div class="mb-2"> <div class="mb-2" id="import-phrase-password-confirm-section">
<label class="block mb-1">Confirm password:</label> <label class="block mb-1">Confirm password</label>
<input <input
type="password" 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" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="confirm password"
/> />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button <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" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
import wallet Import
</button> </button>
<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" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Back
</button> </button>
</div> </div>
<div <div
id="import-error" id="import-phrase-error"
class="mt-2 border border-border border-dashed p-1 hidden" class="mt-2 border border-border border-dashed p-1 hidden"
></div> ></div>
</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 id="view-main" class="view hidden">
<div <div
class="flex justify-between items-center border-b border-border pb-1 mb-2" class="flex justify-between items-center border-b border-border pb-1 mb-2"
@@ -171,61 +244,128 @@
<button <button
id="btn-settings" id="btn-settings"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="settings" title="Settings"
> >
[=] Settings
</button> </button>
<button <button
id="btn-lock" id="btn-lock"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer"
title="lock" title="Lock wallet"
> >
[x] Lock
</button> </button>
</div> </div>
</div> </div>
<!-- account selector --> <!-- wallet list -->
<div class="mb-2"> <div id="wallet-list"></div>
<div class="flex justify-between items-center">
<select <!-- add wallet button -->
id="account-selector" <div class="mt-3 border-t border-border pt-2">
class="border border-border p-1 font-mono text-sm bg-bg text-fg flex-1 mr-2" <button
></select> id="btn-add-wallet"
<button class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
id="btn-copy-address" >
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" + Add wallet
title="copy address" </button>
>
[cp]
</button>
</div>
<div
id="current-address"
class="text-xs text-muted mt-1 break-all"
></div>
</div> </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 --> <!-- balance -->
<div class="border-b border-border-light pb-2 mb-2"> <div class="border-b border-border-light pb-2 mb-2">
<div class="text-base font-bold"> <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>
<div
class="text-xs text-muted"
id="address-usd-value"
></div>
</div> </div>
<!-- action buttons --> <!-- actions -->
<div class="flex gap-2 mb-3"> <div class="flex gap-2 mb-3">
<button <button
id="btn-send" id="btn-send"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
> >
send Send
</button> </button>
<button <button
id="btn-receive" id="btn-receive"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
> >
receive Receive
</button> </button>
</div> </div>
@@ -239,24 +379,22 @@
id="btn-add-token" id="btn-add-token"
class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" class="border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
> >
[+] + Add
</button> </button>
</div> </div>
<div id="token-list"> <div id="token-list">
<div class="text-muted text-xs py-1"> <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>
</div> </div>
</div> </div>
<!-- ============ SEND VIEW ============ --> <!-- ============ SEND ============ -->
<div id="view-send" class="view hidden"> <div id="view-send" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">Send</h1>
AutistMask [send]
</h1>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Token:</label> <label class="block mb-1">What to send</label>
<select <select
id="send-token" id="send-token"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
@@ -265,16 +403,16 @@
</select> </select>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">To address:</label> <label class="block mb-1">To</label>
<input <input
type="text" type="text"
id="send-to" id="send-to"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="0x..." placeholder="Recipient address (0x...)"
/> />
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Amount:</label> <label class="block mb-1">Amount</label>
<input <input
type="text" type="text"
id="send-amount" id="send-amount"
@@ -283,7 +421,7 @@
/> />
</div> </div>
<div <div
id="send-gas-estimate" id="send-fee-estimate"
class="text-xs text-muted mb-2 hidden" class="text-xs text-muted mb-2 hidden"
></div> ></div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -291,13 +429,13 @@
id="btn-send-confirm" id="btn-send-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
send Send
</button> </button>
<button <button
id="btn-send-back" id="btn-send-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Cancel
</button> </button>
</div> </div>
<div <div
@@ -306,39 +444,46 @@
></div> ></div>
</div> </div>
<!-- ============ RECEIVE VIEW ============ --> <!-- ============ RECEIVE ============ -->
<div id="view-receive" class="view hidden"> <div id="view-receive" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [receive] Receive
</h1> </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 <div
id="receive-address" 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>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
id="btn-receive-copy" id="btn-receive-copy"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
copy to clipboard Copy address
</button> </button>
<button <button
id="btn-receive-back" id="btn-receive-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Back
</button> </button>
</div> </div>
</div> </div>
<!-- ============ ADD TOKEN VIEW ============ --> <!-- ============ ADD TOKEN ============ -->
<div id="view-add-token" class="view hidden"> <div id="view-add-token" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [add token] Add Token
</h1> </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"> <div class="mb-2">
<label class="block mb-1">Token contract address:</label> <label class="block mb-1">Contract address</label>
<input <input
type="text" type="text"
id="add-token-address" id="add-token-address"
@@ -355,13 +500,13 @@
id="btn-add-token-confirm" id="btn-add-token-confirm"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
add token Add
</button> </button>
<button <button
id="btn-add-token-back" id="btn-add-token-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Cancel
</button> </button>
</div> </div>
<div <div
@@ -370,81 +515,50 @@
></div> ></div>
</div> </div>
<!-- ============ SETTINGS VIEW ============ --> <!-- ============ SETTINGS ============ -->
<div id="view-settings" class="view hidden"> <div id="view-settings" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [settings] Settings
</h1> </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"> <div class="mb-3">
<input <input
type="text" type="text"
id="settings-rpc" id="settings-rpc"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="https://..."
/> />
<button <button
id="btn-save-rpc" id="btn-save-rpc"
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
save Save
</button> </button>
</div> </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"> <div class="border-t border-border pt-2">
<button <button
id="btn-settings-back" id="btn-settings-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
back Back
</button> </button>
</div> </div>
</div> </div>
<!-- ============ APPROVAL VIEW ============ --> <!-- ============ APPROVAL ============ -->
<div id="view-approve" class="view hidden"> <div id="view-approve" class="view hidden">
<h1 class="font-bold border-b border-border pb-1 mb-3"> <h1 class="font-bold border-b border-border pb-1 mb-3">
AutistMask [approve request] A website is requesting access
</h1> </h1>
<div class="mb-2"> <div class="mb-2">
<div class="text-xs text-muted mb-1"> <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>
<div class="font-bold mb-1" id="approve-type"></div> <div class="font-bold mb-1" id="approve-type"></div>
</div> </div>
@@ -457,13 +571,13 @@
id="btn-approve" id="btn-approve"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
approve Allow
</button> </button>
<button <button
id="btn-reject" id="btn-reject"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
> >
reject Deny
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,15 @@
// AutistMask popup UI — view management and event wiring // AutistMask popup UI — view management and event wiring
// All wallet logic will live in background/; this file is purely UI. // All wallet logic will live in background/; this file is purely UI.
const views = [ const VIEWS = [
"lock", "lock",
"setup", "welcome",
"create", "create",
"import", "import-phrase",
"import-key",
"main", "main",
"add-wallet",
"address",
"send", "send",
"receive", "receive",
"add-token", "add-token",
@@ -15,7 +18,7 @@ const views = [
]; ];
function showView(name) { function showView(name) {
for (const v of views) { for (const v of VIEWS) {
const el = document.getElementById(`view-${v}`); const el = document.getElementById(`view-${v}`);
if (el) { if (el) {
el.classList.toggle("hidden", v !== name); el.classList.toggle("hidden", v !== name);
@@ -24,15 +27,18 @@ function showView(name) {
} }
// -- mock state (will be replaced by background messaging) -- // -- 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 = { const state = {
locked: true, locked: true,
hasWallet: false, hasWallet: false,
password: null, password: null,
accounts: [], wallets: [],
selectedAccount: 0, selectedWallet: null,
tokens: [], selectedAddress: null,
rpcUrl: "https://eth.llamarpc.com", rpcUrl: "https://eth.llamarpc.com",
mnemonic: null, isFirstSetup: true,
}; };
// -- helpers -- // -- helpers --
@@ -52,63 +58,190 @@ function hideError(id) {
function truncateAddress(addr) { function truncateAddress(addr) {
if (!addr) return ""; if (!addr) return "";
return addr.slice(0, 6) + "..." + addr.slice(-4); return addr.slice(0, 6) + "\u2026" + addr.slice(-4);
} }
function updateAccountSelector() { function makeStubAddress() {
const sel = $("account-selector"); const hex = Array.from({ length: 40 }, () =>
sel.innerHTML = ""; Math.floor(Math.random() * 16).toString(16),
state.accounts.forEach((acct, i) => { ).join("");
const opt = document.createElement("option"); return {
opt.value = i; address: "0x" + hex,
opt.textContent = `Account ${i}: ${truncateAddress(acct.address)}`; balance: "0.0000",
sel.appendChild(opt); tokens: [],
}); };
sel.value = state.selectedAccount;
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
} }
function updateTokenList() { // -- render wallet list on main view --
const list = $("token-list"); function renderWalletList() {
if (state.tokens.length === 0) { const container = $("wallet-list");
list.innerHTML = if (state.wallets.length === 0) {
'<div class="text-muted text-xs py-1">no tokens added</div>'; container.innerHTML =
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
return; 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( .map(
(t) => (t) =>
`<div class="py-1 border-b border-border-light flex justify-between">` + `<div class="py-1 border-b border-border-light flex justify-between">` +
`<span>${t.symbol}</span>` + `<span>${t.symbol}</span>` +
`<span>${t.balance || "0.0000"}</span>` + `<span>${t.balance || "0"}</span>` +
`</div>`, `</div>`,
) )
.join(""); .join("");
} }
function updateSendTokenSelect() { function updateSendTokenSelect(addr) {
const sel = $("send-token"); const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>'; sel.innerHTML = '<option value="ETH">ETH</option>';
state.tokens.forEach((t) => { addr.tokens.forEach((t) => {
const opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = t.address; opt.value = t.contractAddress;
opt.textContent = t.symbol; opt.textContent = t.symbol;
sel.appendChild(opt); 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 -- // -- init --
function 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) { if (!state.hasWallet) {
showView("setup"); showView("welcome");
} else if (state.locked) { } else if (state.locked) {
showView("lock"); showView("lock");
} else { } else {
renderWalletList();
showView("main"); showView("main");
} }
@@ -116,100 +249,119 @@ function init() {
$("btn-unlock").addEventListener("click", () => { $("btn-unlock").addEventListener("click", () => {
const pw = $("unlock-password").value; const pw = $("unlock-password").value;
if (!pw) { if (!pw) {
showError("unlock-error", "enter a password"); showError("unlock-error", "Please enter your password.");
return; return;
} }
hideError("unlock-error"); hideError("unlock-error");
// TODO: send unlock message to background // TODO: send unlock message to background
state.locked = false; state.locked = false;
updateAccountSelector(); renderWalletList();
updateTokenList();
showView("main"); showView("main");
}); });
// -- Setup -- // -- Welcome --
$("btn-setup-create").addEventListener("click", () => { $("btn-welcome-new").addEventListener("click", showCreateView);
// TODO: request mnemonic generation from background $("btn-welcome-recovery").addEventListener("click", () =>
$("create-mnemonic").textContent = showImportView("phrase"),
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; );
showView("create"); $("btn-welcome-key").addEventListener("click", () => showImportView("key"));
});
$("btn-setup-import").addEventListener("click", () => {
showView("import");
});
// -- Create wallet -- // -- Create wallet --
$("btn-create-confirm").addEventListener("click", () => { $("btn-create-confirm").addEventListener("click", () => {
const pw = $("create-password").value; if (
const pw2 = $("create-password-confirm").value; !validatePasswords(
if (!pw) { "create-password",
showError("create-error", "enter a password"); "create-password-confirm",
return; "create-error",
} )
if (pw !== pw2) { ) {
showError("create-error", "passwords do not match");
return; return;
} }
hideError("create-error"); hideError("create-error");
// TODO: send create wallet message to background const walletNum = state.wallets.length + 1;
state.hasWallet = true; addWalletAndGoToMain({
state.locked = false; type: "hd",
state.password = pw; name: "Wallet " + walletNum,
state.mnemonic = $("create-mnemonic").textContent; mnemonic: $("create-mnemonic").textContent,
state.accounts = [ addresses: [makeStubAddress()],
{ });
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
}); });
$("btn-create-back").addEventListener("click", () => { $("btn-create-back").addEventListener("click", () => {
showView("setup"); showView(state.isFirstSetup ? "welcome" : "add-wallet");
}); });
// -- Import wallet -- // -- Import recovery phrase --
$("btn-import-confirm").addEventListener("click", () => { $("btn-import-phrase-confirm").addEventListener("click", () => {
const mnemonic = $("import-mnemonic").value.trim(); const mnemonic = $("import-mnemonic").value.trim();
const pw = $("import-password").value;
const pw2 = $("import-password-confirm").value;
if (!mnemonic) { if (!mnemonic) {
showError("import-error", "enter a seed phrase"); showError(
"import-phrase-error",
"Please enter your recovery phrase.",
);
return; return;
} }
if (!pw) { const words = mnemonic.split(/\s+/);
showError("import-error", "enter a password"); if (words.length !== 12 && words.length !== 24) {
showError(
"import-phrase-error",
"Recovery phrase must be 12 or 24 words. You entered " +
words.length +
".",
);
return; return;
} }
if (pw !== pw2) { if (
showError("import-error", "passwords do not match"); !validatePasswords(
"import-phrase-password",
"import-phrase-password-confirm",
"import-phrase-error",
)
) {
return; return;
} }
hideError("import-error"); hideError("import-phrase-error");
// TODO: validate mnemonic and send to background const walletNum = state.wallets.length + 1;
state.hasWallet = true; addWalletAndGoToMain({
state.locked = false; type: "hd",
state.password = pw; name: "Wallet " + walletNum,
state.mnemonic = mnemonic; mnemonic: mnemonic,
state.accounts = [ addresses: [makeStubAddress()],
{ });
address: "0x0000000000000000000000000000000000000001",
balance: "0.0000",
},
];
state.selectedAccount = 0;
updateAccountSelector();
updateTokenList();
showView("main");
}); });
$("btn-import-back").addEventListener("click", () => { $("btn-import-phrase-back").addEventListener("click", () => {
showView("setup"); 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 -- // -- Main view --
@@ -224,33 +376,50 @@ function init() {
showView("settings"); showView("settings");
}); });
$("account-selector").addEventListener("change", (e) => { $("btn-add-wallet").addEventListener("click", () => {
state.selectedAccount = parseInt(e.target.value, 10); showView("add-wallet");
$("current-address").textContent =
state.accounts[state.selectedAccount]?.address || "";
$("eth-balance").textContent =
state.accounts[state.selectedAccount]?.balance || "0.0000";
}); });
$("btn-copy-address").addEventListener("click", () => { // -- Add wallet menu (from main view) --
const addr = state.accounts[state.selectedAccount]?.address; $("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) { if (addr) {
navigator.clipboard.writeText(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", () => { $("btn-send").addEventListener("click", () => {
updateSendTokenSelect();
$("send-to").value = ""; $("send-to").value = "";
$("send-amount").value = ""; $("send-amount").value = "";
$("send-gas-estimate").classList.add("hidden"); $("send-fee-estimate").classList.add("hidden");
$("send-status").classList.add("hidden"); $("send-status").classList.add("hidden");
showView("send"); showView("send");
}); });
$("btn-receive").addEventListener("click", () => { $("btn-receive").addEventListener("click", () => {
$("receive-address").textContent = const addr = currentAddress();
state.accounts[state.selectedAccount]?.address || ""; $("receive-address").textContent = addr ? addr.address : "";
showView("receive"); showView("receive");
}); });
@@ -266,23 +435,23 @@ function init() {
const to = $("send-to").value.trim(); const to = $("send-to").value.trim();
const amount = $("send-amount").value.trim(); const amount = $("send-amount").value.trim();
if (!to) { if (!to) {
showError("send-status", "enter a recipient address"); showError("send-status", "Please enter a recipient address.");
$("send-status").classList.remove("hidden"); $("send-status").classList.remove("hidden");
return; return;
} }
if (!amount || isNaN(parseFloat(amount))) { if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
showError("send-status", "enter a valid amount"); showError("send-status", "Please enter a valid amount.");
$("send-status").classList.remove("hidden"); $("send-status").classList.remove("hidden");
return; return;
} }
// TODO: construct and send transaction via background // TODO: construct and send transaction via background
const el = $("send-status"); const el = $("send-status");
el.textContent = "transaction sent (stub)"; el.textContent = "Sent! (stub)";
el.classList.remove("hidden"); el.classList.remove("hidden");
}); });
$("btn-send-back").addEventListener("click", () => { $("btn-send-back").addEventListener("click", () => {
showView("main"); showAddressDetail();
}); });
// -- Receive -- // -- Receive --
@@ -294,30 +463,35 @@ function init() {
}); });
$("btn-receive-back").addEventListener("click", () => { $("btn-receive-back").addEventListener("click", () => {
showView("main"); showAddressDetail();
}); });
// -- Add Token -- // -- Add Token --
$("btn-add-token-confirm").addEventListener("click", () => { $("btn-add-token-confirm").addEventListener("click", () => {
const addr = $("add-token-address").value.trim(); const contractAddr = $("add-token-address").value.trim();
if (!addr || !addr.startsWith("0x")) { if (!contractAddr || !contractAddr.startsWith("0x")) {
showError("add-token-error", "enter a valid contract address"); showError(
"add-token-error",
"Please enter a valid contract address starting with 0x.",
);
return; return;
} }
hideError("add-token-error"); hideError("add-token-error");
// TODO: look up token name/symbol/decimals from contract via background // TODO: look up token name/symbol/decimals from contract via background
state.tokens.push({ const addr = currentAddress();
address: addr, if (addr) {
symbol: "TKN", addr.tokens.push({
decimals: 18, contractAddress: contractAddr,
balance: "0.0000", symbol: "TKN",
}); decimals: 18,
updateTokenList(); balance: "0",
showView("main"); });
}
showAddressDetail();
}); });
$("btn-add-token-back").addEventListener("click", () => { $("btn-add-token-back").addEventListener("click", () => {
showView("main"); showAddressDetail();
}); });
// -- Settings -- // -- Settings --
@@ -326,45 +500,21 @@ function init() {
// TODO: persist via background // 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", () => { $("btn-settings-back").addEventListener("click", () => {
updateAccountSelector(); renderWalletList();
updateTokenList();
showView("main"); showView("main");
}); });
// -- Approval -- // -- Approval --
$("btn-approve").addEventListener("click", () => { $("btn-approve").addEventListener("click", () => {
// TODO: send approval to background // TODO: send approval to background
renderWalletList();
showView("main"); showView("main");
}); });
$("btn-reject").addEventListener("click", () => { $("btn-reject").addEventListener("click", () => {
// TODO: send rejection to background // TODO: send rejection to background
renderWalletList();
showView("main"); showView("main");
}); });
} }