diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 19:14:38 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 19:14:38 -0700 |
| commit | 4409629ab28372def6afb07c2fb1e124a58ab171 (patch) | |
| tree | 65474cfb28b7ce8fd1602e141aac01cacef52d8a /src | |
| parent | de9efec9205f5faa0245d1d0182e87931f2d26c2 (diff) | |
| download | coinosv3-4409629ab28372def6afb07c2fb1e124a58ab171.tar.gz coinosv3-4409629ab28372def6afb07c2fb1e124a58ab171.zip | |
Logging out leaves one identity, not everyone on the device.
With several identities signed in, the profile's Log out now signs out
only the current one: its accounts leave the session, directory, cache
and vault, the others stay put, and the next identity takes the screen.
The confirmation names who is leaving and says whether they can sign
straight back in (a nostr login) or need their recovery phrase (a bare
seed). Logging out of every account is a second, explicit button. Alone
on the device, Log out is unchanged.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 35 | ||||
| -rw-r--r-- | src/features/messages.js | 25 | ||||
| -rw-r--r-- | src/i18n.js | 4 |
3 files changed, 53 insertions, 11 deletions
@@ -1495,14 +1495,17 @@ function identityPkOf(a) { // Both are read from the account itself; the key remembered on the record // (saveDirectory) and in the durable directory by xpub is only the fallback, // for a watch-only entry whose seed isn't here to ask. +// One key per person: a seed's Spending and Savings accounts share it. +const seedKeyOf = (a) => a.type === 'watch' ? 'w:' + a.xpub + : a.mnemonic ? 'm:' + a.mnemonic + '\n' + (a.passphrase || '') + '\n' + (a.deriveIndex || 0) + : 'x:' + (a.xprv || a.xpub || a.id); + function identities() { const pkOf = identityPkOf; const bySeed = new Map(); for (const a of accounts) { if (a.provisional) continue; - const key = a.type === 'watch' ? 'w:' + a.xpub - : a.mnemonic ? 'm:' + a.mnemonic + '\n' + (a.passphrase || '') + '\n' + (a.deriveIndex || 0) - : 'x:' + (a.xprv || a.xpub || a.id); + const key = seedKeyOf(a); const active = a.id === activeId; const pk = pkOf(a); const cur = bySeed.get(key); @@ -1527,6 +1530,30 @@ function clearFeatureNav() { ui.chatOpen = false; ui.userSearch = null; ui.noteThread = null; ui.zapSetup = null; } +// Sign out of the CURRENT identity only. "Log out" locks every account on +// the device, which is the wrong size when two people (or two of your own +// keys) are signed in and just one should leave. This removes that +// identity's accounts — session, directory, cache, vault — and leaves the +// others exactly as they were, landing on the next one. A nostr login can +// sign straight back in (its seed lives on the relays, encrypted to its +// key); a bare seed wallet needs its recovery phrase, and the confirmation +// says which. +function signOutIdentity() { + const me = activeAccount(); + if (!me) return; + const key = seedKeyOf(me); + const mine = accounts.filter((a) => seedKeyOf(a) === key).map((a) => a.id); + clearFeatureNav(); + // the active one last: wiping it is what hands off to the next identity + for (const id of mine.filter((id) => id !== me.id)) wipeAccount(id); + wipeAccount(me.id); + if (!activeAccount()) { ui.screen = 'unlock'; ui.unlockTab = 'create'; ui.onb = { step: 'welcome' }; } + render(); +} +// Whether the current identity can come back without a recovery phrase: +// a nostr login (extension, bunker, passkey, Google) re-derives its wallet. +const identityRecoverable = () => !!loginPkOf(activeAccount() || {}); + // Switch to another identity already signed in on this device: the wallet // it opens lands on its own history, out of any profile or chat the previous // identity had open. @@ -5282,7 +5309,7 @@ const ctx = { // this runs — wipe now and land on the front door. // identity switching lives on the profile page (the Accounts screen is // about Spending/Savings faces, not who you are) - signInAnother, identities, switchIdentity, + signInAnother, identities, switchIdentity, signOutIdentity, identityRecoverable, logoutForget: () => { ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; ui.chatOpen = false; ui.pubProf = null; diff --git a/src/features/messages.js b/src/features/messages.js index 127dc08..e2d09b6 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4045,19 +4045,30 @@ export function messagesFeature(ctx) { onClick: () => { ui.logoutConfirm = null; ctx.logoutForget(); }, }, t('clearAll')), h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = true; render(); } }, t('back'))) - : h('div', { class: 'card col confirm-pop', style: 'gap:10px' }, - h('h3', { style: 'margin:0' }, t('logout') + '?'), - h('p', { class: 'small muted', style: 'margin:0' }, t('logoutPopBlurb')), - h('button', { class: 'btn-primary btn-block', onClick: () => { + : (() => { + // With other identities signed in, logging out means THIS one + // leaves and the rest stay put; logging out of everything is + // the second, explicit choice. Alone on the device, it's the + // plain log-out it always was. + const others = ctx.identities ? ctx.identities().filter((x) => !x.active) : []; + const one = others.length > 0 && !!ctx.signOutIdentity; + const leave = (fn) => () => { ui.logoutConfirm = null; ui.profilePk = null; ui.profEdit = null; ui.profEditFilled = false; - ctx.logout && ctx.logout(); - } }, t('logout')), + fn && fn(); + }; + return h('div', { class: 'card col confirm-pop', style: 'gap:10px' }, + h('h3', { style: 'margin:0' }, (one ? t('logoutOne', { name: displayName(pk) }) : t('logout')) + '?'), + h('p', { class: 'small muted', style: 'margin:0' }, + one ? t(ctx.identityRecoverable && ctx.identityRecoverable() ? 'logoutOneBlurbLogin' : 'logoutOneBlurbSeed') : t('logoutPopBlurb')), + h('button', { class: 'btn-primary btn-block', onClick: leave(one ? ctx.signOutIdentity : ctx.logout) }, t('logout')), + one ? h('button', { class: 'btn-block', onClick: leave(ctx.logout) }, t('logoutAll')) : null, ctx.logoutForget ? h('button', { class: 'btn-block', style: 'color:var(--red,#c0392b)', onClick: () => { ui.logoutConfirm = 'forget'; render(); }, }, t('logoutForget')) : null, - h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = null; render(); } }, t('back')))); + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.logoutConfirm = null; render(); } }, t('back'))); + })()); // The identity switcher: every account signed in on this device as an // avatar + name row (tap = switch, nothing logged out), and the door to // add a new one. diff --git a/src/i18n.js b/src/i18n.js index 7948dfc..bc3cf14 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -57,6 +57,10 @@ const STR = { entropyWarn: '⚠ Anyone who can guess this text can take your money. Use plenty of real-world randomness — 100+ dice rolls, or the order of a fully shuffled deck of cards.', entropyPlaceholder: '4 2 6 6 1 3 5 2 …', logoutPopBlurb: 'Your accounts stay saved on this device.', + logoutOne: 'Log out of {name}', + logoutOneBlurbLogin: 'Only this account leaves — the others stay signed in. You can sign back in with the same key anytime.', + logoutOneBlurbSeed: 'Only this account leaves — the others stay signed in. It is removed from this device, so make sure you have its recovery phrase.', + logoutAll: 'Log out of all accounts', clearAllWarn: '⚠ This removes every account from this device. Any account whose recovery phrase or key you have not backed up will be lost for good.', removeWalletTitle: 'Remove account', removeWalletWarn: '⚠ Remove “{name}” from this device? It will be gone from this browser — make sure you have its recovery phrase. Other accounts aren’t affected.', |
