diff options
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 74 |
1 files changed, 73 insertions, 1 deletions
@@ -3209,7 +3209,9 @@ function settingsTab() { switch (ui.settingsPage) { // the recovery phrase first: it is the one thing on this page that // matters if the phone is lost tonight - case 'wallet': return page(a ? [recoveryCard(a), walletNameCard(a), pubkeyCard(a), historyExportCard()] : []); + case 'wallet': + if (ui.delAcct && a) return deleteAccountView(a); + return page(a ? [recoveryCard(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 @@ -3777,6 +3779,76 @@ function openAccountSettings(id) { // Per-wallet settings cards, shared by the per-account screen (Accounts → ⚙) // and the main Settings tab (which inlines them for the active wallet). +// Delete account: the server forgets this wallet (its name is freed, its +// records go) and the wallet leaves this device. "someone's asking for a way +// to delete their v3 account. we allowed that in v2" (Adam, 2026-11-10). The +// coins are the user's own and no server can delete them: they stay +// spendable from the recovery phrase, which is why the page says how much is +// still here before anything happens. +// The saved copy on this device has to go too, and the vault can only be +// rewritten with its password in hand: a blank one is tried here, a real one +// is known once the app has been unlocked with it this session. +function vaultWritable() { + if (vaultPassword != null || !hasVault()) return true; + try { const list = decryptVault(loadVaultBlob(), ''); vaultPassword = ''; mergeVaultList(list); return true; } catch { return false; } +} +function deleteAccountCard(a) { + if (a.type === 'watch' || wallet.watchOnly || !wallet.nostrSign) return null; + return h('div', { class: 'card col', 'data-key': 'delete-account' }, + h('h3', {}, t('delAcctTitle')), + h('p', { class: 'small muted', style: 'margin:0' }, t('delAcctBlurb')), + h('button', { class: 'btn-danger btn-block', onClick: () => { + if (!vaultWritable()) { toast(t('delAcctUnlockFirst')); return; } + const d = ui.delAcct = { accId: a.id, info: null, sure: false, busy: false, error: '' }; + render(); + Promise.resolve(featureHook('accountDelete', true)).then((info) => { if (ui.delAcct === d) { d.info = info || {}; render(); } }) + .catch((e) => { if (ui.delAcct === d) { d.info = {}; d.error = e.message; render(); } }); + } }, t('delAcctTitle'))); +} +function deleteAccountView(a) { + const d = ui.delAcct; + const heldSat = (wallet.spendable || 0) + (featureHook('arkHeldSat') || 0); + const info = d.info; + const names = (info && info.names) || []; + const owedSat = info ? (info.pendingSat || 0) + (info.withdrawSat || 0) : 0; + const blocked = !!(info && (info.blocked || owedSat > 0 || info.paying)); + const back = () => { ui.delAcct = null; render(); }; + const go = async () => { + if (d.busy || !d.sure || blocked) return; + d.busy = true; d.error = ''; render(); + try { + const r = await featureHook('accountDelete', false); + if (r && r.blocked) { d.info = { ...d.info, ...r, blocked: true }; d.sure = false; return; } + ui.delAcct = null; ui.settingsPage = null; + wipeAccountCache(a); + // out of the saved vault first, or the next unlock brings it back + if (a.persisted && vaultWritable()) { a.persisted = false; writeVault(); } + removeAccount(a.id); + toast(t('delAcctDone')); + } catch (e) { d.error = e.message; } + finally { d.busy = false; render(); } + }; + return h('div', { class: 'col', style: 'gap:16px' }, + h('div', { class: 'card col', style: 'gap:10px' }, + h('h3', {}, t('delAcctTitle')), + !info ? h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('delAcctChecking'))) : null, + h('ul', { class: 'small', style: 'margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px' }, + names.length ? h('li', {}, t('delAcctName', { name: names.join(', ') })) : null, + h('li', {}, t('delAcctRecords')), + h('li', {}, t('delAcctDevice', { name: seedName(a) })), + h('li', { class: 'muted' }, t('delAcctStays'))), + blocked ? h('div', { class: 'warn-box' }, t('delAcctOwed', { sat: fmtAmount(owedSat) })) : null, + heldSat > 0 ? h('div', { class: 'warn-box' }, t('delAcctFunds', { sat: fmtAmount(heldSat) })) : null, + d.error ? h('div', { class: 'warn-box' }, d.error) : null, + h('label', { class: 'row gap6 small', style: 'align-items:flex-start;cursor:pointer' }, + h('input', { type: 'checkbox', checked: d.sure, disabled: blocked || d.busy, style: 'margin-top:2px', onChange: (e) => { d.sure = e.target.checked; render(); } }), + h('span', {}, t(heldSat > 0 ? 'delAcctSureFunds' : 'delAcctSure'))), + h('div', { class: 'row gap6' }, + h('button', { class: 'btn-ghost grow', disabled: d.busy, onClick: back }, t('back')), + h('button', { class: 'btn-danger grow', disabled: !info || !d.sure || blocked || d.busy, onClick: go }, + d.busy ? h('span', { class: 'spinner sm' }) : t('delAcctGo'))))); +} + function recoveryCard(a) { const isWatch = a.type === 'watch' || (!a.mnemonic && !a.xprv); const shown = ui.revealShown; |
