From ff0d637911051b45f54d54ece54aa16387c0fd6f Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 26 Jun 2026 13:08:11 -0700 Subject: Logo returns to the wallet's Receive page; add paste-from-clipboard buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - goHome: when a wallet is open, the logo goes to its Receive page (its tabs) instead of the create/import screen — so it's always a way back to your wallet. Only with no wallet open does it show create/import. - Paste buttons: a clipboard 'Paste' icon on the send address field and the import (seed/key/xpub) and 'load seed to spend' fields, filling them from the clipboard. Hidden where the Clipboard read API isn't available. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.js | 71 ++++++++++++++++++++++++++++++++++++++++--------------------- src/i18n.js | 1 + 2 files changed, 48 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 333b0ef..1f45a0a 100644 --- a/src/app.js +++ b/src/app.js @@ -299,6 +299,18 @@ function copyBtn(text, label = t('copy')) { return h('button', { class: 'btn-sm', onClick: () => copy(text) }, label); } +// A small "paste from clipboard" button; apply(text) receives the trimmed text. +// Returns null where the Clipboard read API isn't available (the catch keeps it +// silent if a browser blocks the read). +function pasteBtn(apply) { + if (typeof navigator === 'undefined' || !navigator.clipboard || !navigator.clipboard.readText) return null; + return h('button', { + type: 'button', class: 'btn-sm', title: t('paste'), + onClick: async () => { try { const txt = await navigator.clipboard.readText(); if (txt) apply(txt.trim()); } catch {} }, + html: '', + }); +} + // ---------------------------------------------------------------- display unit // Global BTC/sats preference, persisted in localStorage across refreshes and // logouts. Every unit label on the site is clickable to toggle it. @@ -537,21 +549,25 @@ function pickConfirm(words) { } function importPane() { + const ta = h('textarea', { + placeholder: t('importPlaceholder'), + autocapitalize: 'none', + autocomplete: 'off', + spellcheck: 'false', + value: ui.importText, + onInput: (e) => (ui.importText = e.target.value), + }); return h( 'div', { class: 'col' }, h( 'label', { class: 'field' }, - h('span', { class: 'lab' }, t('importLabel')), - h('textarea', { - placeholder: t('importPlaceholder'), - autocapitalize: 'none', - autocomplete: 'off', - spellcheck: 'false', - value: ui.importText, - onInput: (e) => (ui.importText = e.target.value), - }) + h('div', { class: 'row between' }, + h('span', { class: 'lab' }, t('importLabel')), + pasteBtn((text) => { ta.value = text; ui.importText = text; }) + ), + ta ), optionsPanel(), h('button', { class: 'btn-primary btn-block', onClick: () => openWallet(ui.importText) }, t('openWallet')) @@ -1681,14 +1697,15 @@ function goHome() { persistAccounts(); activeId = accounts[0] ? accounts[0].id : null; } - if (ui.screen === 'howItWorks') { - ui.screen = ui.returnScreen === 'wallet' ? 'wallet' : 'unlock'; - } - if (ui.screen === 'wallet') { + // A wallet is open → home is its Receive page (not the create/import screen), + // so the logo is always a way back to your wallet. + if (activeAccount()) { + ui.screen = 'wallet'; ui.tab = wallet.offline ? 'settings' : 'receive'; ui.draft = null; ui.sendResult = null; ui.sendError = ''; + ui.fromWallet = false; } else { ui.screen = 'unlock'; ui.unlockTab = 'create'; @@ -2153,16 +2170,20 @@ function receiveTab() { // account's xpub — you can't load the wrong wallet's seed. function loadSeedCard() { const ls = ui.loadSeed; + const seedTa = h('textarea', { + class: 'mono-input', rows: '3', placeholder: t('seedPlaceholder'), + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ls.value, + onInput: (e) => { ls.value = e.target.value; }, + }); return h( 'div', { class: 'card col' }, - h('h3', {}, t('loadSeedTitle')), + h('div', { class: 'row between' }, + h('h3', { style: 'margin:0' }, t('loadSeedTitle')), + pasteBtn((text) => { seedTa.value = text; ls.value = text; }) + ), h('p', { class: 'small muted', style: 'margin:0' }, t('loadSeedDesc')), - h('textarea', { - class: 'mono-input', rows: '3', placeholder: t('seedPlaceholder'), - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ls.value, - onInput: (e) => { ls.value = e.target.value; }, - }), + seedTa, h('input', { type: 'password', class: 'mono-input', placeholder: t('bip39PassphraseOpt'), autocapitalize: 'none', autocomplete: 'off', value: ls.passphrase, @@ -2458,15 +2479,17 @@ function recipientRow(s, r, i) { check.style.display = a ? '' : 'none'; }; + const addrInput = h('input', { + type: 'text', class: 'mono-input grow', placeholder: 'bc1q…', + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, + onInput: (e) => { r.address = e.target.value; syncCheck(); }, + }); const row = h( 'div', { class: 'col gap6' }, h('div', { class: 'input-group' }, - h('input', { - type: 'text', class: 'mono-input grow', placeholder: 'bc1q…', - autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, - onInput: (e) => { r.address = e.target.value; syncCheck(); }, - }), + addrInput, + pasteBtn((text) => { addrInput.value = text; r.address = text; syncCheck(); }), i === 0 && canScan() && h('button', { type: 'button', class: 'btn-sm', title: t('scanQr'), onClick: scanIntoSend, html: '', diff --git a/src/i18n.js b/src/i18n.js index 84ef7b3..b96f536 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -103,6 +103,7 @@ const STR = { reenterPassphrase: 'Re-enter passphrase', back: '← Back', cancel: 'Cancel', + paste: 'Paste', openWallet: 'Open wallet', wordsMismatch: 'Those words don’t match your phrase. Check your backup.', passphraseMismatch: 'That passphrase doesn’t match the one you entered.', -- cgit v1.2.3