aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js74
1 files changed, 73 insertions, 1 deletions
diff --git a/src/app.js b/src/app.js
index 23a1d42..95909d0 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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;