aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js19
-rw-r--r--src/i18n.js3
-rw-r--r--tools/balance-settings-row-test.js65
3 files changed, 86 insertions, 1 deletions
diff --git a/src/app.js b/src/app.js
index 50aa5c8..e6e6843 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3213,7 +3213,7 @@ function settingsTab() {
// matters if the phone is lost tonight
case 'wallet':
if (ui.delAcct && a) return deleteAccountView(a);
- return page(a ? [recoveryCard(a), walletNameCard(a), pubkeyCard(a), historyExportCard(), deleteAccountCard(a)] : []);
+ return page(a ? [recoveryCard(a), balanceSettingsCard(a), walletNameCard(a), pubkeyCard(a), historyExportCard(), deleteAccountCard(a)] : []);
case 'payments': return page(featureAll('settingsCards').reverse());
case 'pos': return page(featureAll('posSettingsCards').reverse()); // the till's own card first, then the terminal's
case 'network': return page([networkCard(), explorerCard()]); // an old bookmark of the tile; the cards live under Advanced now
@@ -3906,6 +3906,23 @@ function recoveryCard(a) {
);
}
+// The balance cards' gears, reachable from Settings → Wallet too: "I simply
+// cannot find this option" (Nils, 2026-11-12) about the Spending card's gear.
+function balanceSettingsCard(a) {
+ if (a.id !== activeId) return null;
+ const views = a.kind ? [a.kind] : spendingActive() ? ['spending', 'savings'] : ['savings'];
+ const row = (view) => h('button', {
+ class: 'btn-ghost btn-block balance-settings-row', 'data-view': view,
+ style: 'display:flex;align-items:center;gap:10px;text-align:left;padding:10px 4px',
+ onClick: () => openWalletSettings(view),
+ },
+ h('span', { style: 'display:flex;color:var(--muted)', html: GEAR_ICON(18) }),
+ h('span', { class: 'col', style: 'flex:1;gap:2px' },
+ h('span', { style: 'font-weight:600' }, viewLabel(a, view)),
+ h('span', { class: 'small muted' }, t(view === 'spending' ? 'spendingSettingsDesc' : 'savingsSettingsDesc'))),
+ h('span', { class: 'muted' }, '›'));
+ return h('div', { class: 'card col', style: 'gap:4px' }, h('h3', {}, t('balanceSettingsTitle')), ...views.map(row));
+}
function walletNameCard(a) {
const saveName = () => {
const v = (ui.editLabel || '').trim();
diff --git a/src/i18n.js b/src/i18n.js
index 1d14a36..194ce40 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1224,6 +1224,9 @@ const STR = {
explorerUrlHint: 'Esplora / electrs REST base, e.g. https://mempool.space/api',
walletName: 'Account name',
walletSettings: 'Account settings',
+ balanceSettingsTitle: 'Balance settings',
+ spendingSettingsDesc: 'Coins, renewals, who holds them, moving them out',
+ savingsSettingsDesc: 'Lock, password, recovery phrase, public key',
securityTitle: 'Security',
walletSettingsDesc: 'Name, recovery phrase, public key, and auto-logout for this account.',
autolockTitle: 'Log out automatically',
diff --git a/tools/balance-settings-row-test.js b/tools/balance-settings-row-test.js
new file mode 100644
index 0000000..89e68d9
--- /dev/null
+++ b/tools/balance-settings-row-test.js
@@ -0,0 +1,65 @@
+// Settings → Wallet lists Spending and Savings; each row opens the page the
+// balance card's gear opens. "I simply cannot find this option" (Nils,
+// 2026-11-12) — the Spending gear was the only way in.
+// Run: bun tools/balance-settings-row-test.js (NO_BUILD=1 to reuse dist/)
+import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+let ok = true;
+const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
+if (!process.env.NO_BUILD && Bun.spawnSync(['bun', 'build.js']).exitCode !== 0) throw new Error('build failed');
+const server = Bun.serve({ port: 0, fetch: async (req) => {
+ const path = new URL(req.url).pathname;
+ const f = Bun.file('dist' + path);
+ return path.includes('.') && await f.exists() ? new Response(f) : new Response(Bun.file('dist/index.html'));
+} });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+const errs = [];
+// mainnet so Spending turns on (the Ark server's info is read); every other
+// outside request is refused, so nothing is claimed or published
+await page.setRequestInterception(true);
+page.on('request', (r) => { const u = new URL(r.url()); (u.origin === server.url.origin || u.hostname === 'ark.coinos.io') ? r.continue() : r.abort(); });
+page.on('pageerror', (e) => errs.push(String(e)));
+const click = (x) => page.evaluate((s) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(s)); if (e) e.click(); return !!e; }, x);
+const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; };
+const openWalletPage = async () => {
+ await page.evaluate(() => [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings').click()); await sleep(500);
+ await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find((e) => /^Wallet/.test(e.textContent.trim())).click()); await sleep(500);
+};
+try {
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'mainnet'));
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await sleep(400);
+ await click('create a new wallet'); await sleep(600);
+ await seedWords(page);
+ await skipSeedCheck(page);
+ if (await waitText('claim a username', 8000)) { await click('not now'); await sleep(800); }
+ await waitText('receive');
+ await sleep(1500);
+ check('savings-only wallet lists Savings alone', await (async () => { await openWalletPage(); const v = await page.$$eval('.balance-settings-row', (l) => l.map((e) => e.dataset.view)); await page.goBack(); await page.goBack(); await sleep(500); return v.join() === 'savings'; })());
+ await click('set up a spending account'); await sleep(800);
+ if (await waitText('claim a username', 5000)) { await click('not now'); await sleep(800); }
+ await waitText('receive'); await sleep(800);
+ if (process.env.DEBUG) console.log((await page.evaluate(() => document.body.innerText)).slice(0, 600));
+ const gears = await page.$$eval('.balance-gear', (l) => l.length);
+ await openWalletPage();
+ if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.balance-settings-row').closest('.card').scrollIntoView()); await page.screenshot({ path: process.env.SHOT }); }
+ const views = await page.$$eval('.balance-settings-row', (l) => l.map((e) => e.dataset.view));
+ check('Settings → Wallet lists the balances', views.length >= 1, views.join(','));
+ check('Spending is listed when the card shows it', views.includes('spending'), `gears on the wallet: ${gears}`);
+ await page.evaluate(() => document.querySelector('.balance-settings-row[data-view="spending"]')?.click()); await sleep(600);
+ const st = await page.evaluate(() => ({ screen: history.state.nav.screen, view: history.state.nav.settingsView, h3: [...document.querySelectorAll('h3')].map((e) => e.textContent) }));
+ check('the Spending row opens the Spending settings', st.screen === 'accountSettings' && st.view === 'spending' && st.h3.includes('Spending'), JSON.stringify(st.h3.slice(0, 4)));
+ await page.goBack(); await sleep(600);
+ check('Back returns to Settings → Wallet', !!(await page.$('.balance-settings-row')));
+ await page.evaluate(() => document.querySelector('.balance-settings-row[data-view="savings"]')?.click()); await sleep(600);
+ const sv = await page.evaluate(() => ({ screen: history.state.nav.screen, view: history.state.nav.settingsView }));
+ check('the Savings row opens the Savings settings', sv.screen === 'accountSettings' && sv.view === 'savings');
+ check('no page errors', errs.length === 0, errs.join(' | '));
+} finally {
+ await browser.close(); server.stop(true);
+}
+console.log(ok ? 'PASS' : 'FAIL');
+process.exit(ok ? 0 : 1);