diff options
| -rw-r--r-- | src/app.js | 25 | ||||
| -rw-r--r-- | src/features/messages.js | 7 | ||||
| -rw-r--r-- | tools/lock-wallet-test.js | 9 | ||||
| -rw-r--r-- | tools/padlock-ask-test.js | 14 | ||||
| -rw-r--r-- | tools/padlock-flow-test.js | 25 |
5 files changed, 51 insertions, 29 deletions
@@ -2453,10 +2453,14 @@ function softLock() { toast(t('lockedToast')); } +// Lock (or unlock) the wallets on this device — a row on the Accounts page, +// where the other whole-device actions (log out, clear) live; it used to be +// a header icon, one tap from a mis-tap on every screen. function lockBtn() { + if (!activeAccount()) return null; const locked = wallet.watchOnly && hasVault(); return h('button', { - class: 'header-msgs', + class: 'btn-ghost btn-block', title: locked ? t('unlock') : t('lockWallet'), 'aria-label': locked ? t('unlock') : t('lockWallet'), onClick: locked @@ -2467,7 +2471,7 @@ function lockBtn() { } else { ui.justLocked = false; ui.screen = 'vault'; render(); } } : () => softLock(), - }, h('span', { class: 'hm-ico', style: 'font-size:18px;line-height:1' }, locked ? '\u{1F512}' : '\u{1F513}')); + }, h('span', { style: 'font-size:15px;line-height:1;margin-right:6px' }, locked ? '\u{1F512}' : '\u{1F513}'), locked ? t('unlock') : t('lockWallet')); } // Messages sit one tap away, left of the wallet selector. The dot is presence, @@ -2523,7 +2527,6 @@ function brandHeader(withLock) { searchBtn(), messagesBtn(), settingsBtn(), - lockBtn(), avatarMenu()) : null ); @@ -3056,6 +3059,7 @@ function accountsScreen() { ), h('button', { class: 'btn-block', onClick: () => { ui.addWallet = { kind: 'spending', from: 'new' }; render(); } }, t('addWallet')), h('button', { class: 'btn-block', onClick: signInAnother }, t('signInAnother')), + lockBtn(), activeAccount() ? h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = true; render(); } }, t('logout')) : null, hasVault() ? h('button', { class: 'btn-ghost btn-block', onClick: startChangePw }, t('changePassword')) : null, h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.confirmClear = true; render(); } }, t('clearAll')) @@ -3080,10 +3084,23 @@ function groupAccounts() { // Who a group of wallets belongs to: their face and name, and a mark when // it's the identity you're currently using. One identity on the device needs // no heading at all — there's nothing to tell apart. +// Tapping the identity signs you in as it (its first wallet) and opens its +// profile page — the URL used to take the npub while the list stayed put, +// because the profile only paints over the wallet screen. +function openIdentityProfile(pk) { + const g = groupAccounts().find((x) => x.pk === pk); + const first = g && g.rows[0]; + if (first && first.id !== activeId) { + clearFeatureNav(); + activateAccount(first, { fresh: true }); // the screen flip and feature init are synchronous + } else { clearFeatureNav(); ui.screen = 'wallet'; } + featureHook('openProfile', pk); + render(); +} function identityHeading(pk) { if (!pk || groupAccounts().length < 2) return null; const me = ctx.shownPubkey && ctx.shownPubkey(); - const chip = featureHook('profileChip', pk); + const chip = featureHook('profileChip', pk, undefined, () => openIdentityProfile(pk)); return h('div', { class: 'row gap6', style: 'align-items:center;padding:10px 0 4px;border-bottom:1px solid var(--line)', diff --git a/src/features/messages.js b/src/features/messages.js index 06cb6dd..59fb0a5 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -8489,11 +8489,13 @@ export function messagesFeature(ctx) { fieldWhen: { lud16: follows('lud16'), nip05: follows('nip05') }, }).catch(() => {}); }, - profileChip(pk, size) { + // opens the profile, unless the caller has a step to take first (the + // Accounts page switches to that identity before showing its profile) + profileChip(pk, size, onOpen) { const big = size === 'lg'; return h('span', { class: 'zap-chip' + (big ? ' lg' : ''), - onClick: (e) => { e.stopPropagation(); openProfile(pk); }, + onClick: (e) => { e.stopPropagation(); if (onOpen) onOpen(); else openProfile(pk); }, }, avatar(pk, 'chat-avatar ' + (big ? 'chip-lg' : 'mini'), false), h('span', { class: big ? '' : 'small' }, displayName(pk))); }, @@ -8501,6 +8503,7 @@ export function messagesFeature(ctx) { // the same start, but only the follows of an identity that follows // nobody yet — an established nostr user keeps their own list. identitySignedInNew() { setTimeout(() => seedNewIdentity({ onlyIfNoFollows: true }), 0); return true; }, + openProfile(pk) { openProfile(pk); return true; }, init() { const session = ++feedWarmSession; // this wallet's cached faces, from its own namespace — the keys are diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js index 03db663..13d2b91 100644 --- a/tools/lock-wallet-test.js +++ b/tools/lock-wallet-test.js @@ -82,17 +82,18 @@ try { await sleep(1000); console.log('\n[the padlock still asks — locking needs a password]'); + // the padlock lives on the Accounts page (the balance card's switcher opens it) + await page.evaluate(() => document.querySelector('.balance-switch')?.click()); + await sleep(600); await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((n) => n.title === 'Lock wallet'); if (b) b.click(); }); await sleep(600); txt = await body(); check('padlock asks even after "Not now"', /protect this device/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); await click('button', 'Not now'); await sleep(600); - check('and declining lands home', await waitText('receive')); + check('and declining lands back on the Accounts page', /add account/i.test(await body())); console.log('\n[now with a password]'); - await click('button', 'Wallets'); - await sleep(800); await click('button', 'Change password'); await sleep(800); const fields = await page.$$('input[type=password]'); @@ -138,7 +139,7 @@ try { check('backing out deletes nothing', (await vaultSize()) > 0); console.log('\n[delete all is the destructive one]'); - await click('button', 'Wallets'); + await page.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(800); await click('button', 'Delete all'); await sleep(600); diff --git a/tools/padlock-ask-test.js b/tools/padlock-ask-test.js index e2837e5..f1fa3cf 100644 --- a/tools/padlock-ask-test.js +++ b/tools/padlock-ask-test.js @@ -35,27 +35,25 @@ try { await click('button', 'Open wallet'); await waitText('receive', 20000); - // open my own profile (the header avatar) - await page.evaluate(() => document.querySelector('.header-avatar')?.click()); + // open the Accounts page (the balance card's switcher) — the padlock lives there + await page.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(900); - const onProfile = await page.evaluate(() => !!document.querySelector('.profile-avatar, .chat-avatar.profile-avatar')); - check('profile page is open', onProfile, (await bodyText()).slice(0, 60).replace(/\n/g, ' | ')); + check('Accounts page is open', /add account/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | ')); // tap the padlock — the ask must appear without navigating anywhere const tapped = await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /lock/i.test(e.getAttribute('aria-label') || '')); if (!b) return false; b.click(); return true; }); - check('padlock is reachable from the profile page', tapped); + check('padlock is reachable from the Accounts page', tapped); await sleep(500); const text = await bodyText(); check('the ask appears on the tap', /protect this device/i.test(text), text.split('\n').slice(0, 3).join(' | ')); - // declining lands back on the profile, not somewhere else + // declining lands back on the Accounts page, not somewhere else await click('button', 'not now'); await sleep(600); - const back = await page.evaluate(() => !!document.querySelector('.profile-avatar')); - check('declining lands back on the profile page', back, (await bodyText()).slice(0, 60).replace(/\n/g, ' | ')); + check('declining lands back on the Accounts page', /add account/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | ')); } finally { await browser.close(); server.stop(true); } diff --git a/tools/padlock-flow-test.js b/tools/padlock-flow-test.js index e36d7ec..e67711f 100644 --- a/tools/padlock-flow-test.js +++ b/tools/padlock-flow-test.js @@ -21,6 +21,8 @@ await page.setViewport({ width: 420, height: 880 }); const click = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, t); const clickTitle = (t) => page.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, t); +// the padlock lives on the Accounts page: open it from the balance card first +const padlock = async (pg, title) => { await pg.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(500); return pg.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, title); }; const body = () => page.evaluate(() => document.body.innerText); const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await body()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; @@ -40,7 +42,7 @@ try { await sleep(2000); console.log('\n[the Wallets list before opting into Spending]'); - await click('button', 'Wallets'); + await page.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); let txt = await body(); check('shows the on-chain wallet', /savings/i.test(txt), txt.slice(0, 140).replace(/\n+/g, ' | ')); @@ -50,7 +52,7 @@ try { check('back home', await waitText('receive')); console.log('\n[padlock with no password: declining is free]'); - await clickTitle('Lock wallet'); + await padlock(page, 'Lock wallet'); await sleep(500); txt = await body(); check('it asks for a password in place', (await page.$$('input[type=password]')).length >= 2, txt.slice(0, 120).replace(/\n+/g, ' | ')); @@ -58,8 +60,7 @@ try { await click('button', 'Not now'); await sleep(600); txt = await body(); - check('declining lands back home', /receive/i.test(txt) && /balance/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); - check('not on the wallets list', !/add wallet|delete all/i.test(txt)); + check('declining lands back where the padlock was tapped (the Accounts page)', /add account/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); console.log('\n[the logo still means home]'); await page.evaluate(() => document.querySelector('.brand')?.click()); @@ -68,7 +69,7 @@ try { check('logo goes home, not to a password prompt', /receive/i.test(txt) && !/unlock saved|enter your password/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); console.log('\n[setting a password locks in place]'); - await clickTitle('Lock wallet'); + await padlock(page, 'Lock wallet'); await sleep(500); const fields = await page.$$('input[type=password]'); check('password form is up', fields.length >= 2, `${fields.length} field(s)`); @@ -78,11 +79,13 @@ try { await sleep(1200); txt = await body(); check('still home (watch-only), not signed out', /receive/i.test(txt) && /balance/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); + await page.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(500); const closed = await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.title && /unlock/i.test(b.title))); check('padlock is closed', closed); + await page.evaluate(() => document.querySelector('.brand')?.click()); await sleep(400); console.log('\n[and the padlock now asks for the password]'); - await clickTitle('Unlock'); + await padlock(page, 'Unlock'); await sleep(600); txt = await body(); check('unlock needs the password', /password/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); @@ -104,7 +107,7 @@ try { return waitText2('receive'); }; const addWallet = async () => { - await click2('Wallets'); await sleep(600); + await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); await click2('Add wallet'); await sleep(400); await click2('Savings'); await sleep(200); await click2('Continue'); await sleep(600); @@ -126,7 +129,7 @@ try { await sleep(1000); check('wallet B open', await addWallet()); await sleep(2000); - await clickTitle2('Lock wallet'); + await padlock(p2, 'Lock wallet'); await sleep(600); txt = await body2(); check('padlock offers to SET a password (B is saved, vault is passwordless)', @@ -148,7 +151,7 @@ try { await click2('Unlock'); check('password opens it', await waitText2('receive')); await sleep(1000); - await click2('Wallets'); await sleep(600); + await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); check('both wallets survived the re-encrypt', (await savingsRows()) === 2, `${await savingsRows()} row(s)`); await click2('Back'); await sleep(500); @@ -158,7 +161,7 @@ try { await sleep(1000); check('wallet C open', await addWallet()); await sleep(2000); - await clickTitle2('Lock wallet'); + await padlock(p2, 'Lock wallet'); await sleep(600); txt = await body2(); check('it says WHICH password it wants', /already saved on this device/i.test(txt), txt.slice(0, 160).replace(/\n+/g, ' | ')); @@ -179,7 +182,7 @@ try { await click2('Unlock'); check('unlocked once more', await waitText2('receive')); await sleep(1000); - await click2('Wallets'); await sleep(600); + await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); check('all three wallets are in the vault', (await savingsRows()) === 3, `${await savingsRows()} row(s)`); await ctx2.close(); } catch (e) { |
