aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app.js25
-rw-r--r--src/features/messages.js7
-rw-r--r--tools/lock-wallet-test.js9
-rw-r--r--tools/padlock-ask-test.js14
-rw-r--r--tools/padlock-flow-test.js25
5 files changed, 51 insertions, 29 deletions
diff --git a/src/app.js b/src/app.js
index 196d90d..8ac38ce 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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) {