aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-04 09:46:54 -0700
committerAdam Soltys <[email protected]>2026-08-04 09:46:54 -0700
commit399a100248792fe8d6aaf19849eab3ee3baa1984 (patch)
tree6afb67e1801997f63eb278c384b1d424bed4371c /src/features
parente462bc1e7756dcbf7c651ee282e96ee8c48b5a14 (diff)
downloadcoinosv3-399a100248792fe8d6aaf19849eab3ee3baa1984.tar.gz
coinosv3-399a100248792fe8d6aaf19849eab3ee3baa1984.zip
The Nostr card owns its built-in account; the exported key opens this wallet anywhere
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01M5kFodm9azZJVTQxnkDdKe
Diffstat (limited to 'src/features')
-rw-r--r--src/features/nostrlogin.js77
1 files changed, 73 insertions, 4 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 222b516..4c28ac5 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -9,7 +9,7 @@
// published, because it means the nostr key can recover the money.
import { newMnemonic } from '../wallet.js';
-import { npubOf } from '../nostr.js';
+import { npubOf, nsecOf } from '../nostr.js';
import { t } from '../i18n.js';
import { qrSvg } from '../qr.js';
import {
@@ -289,6 +289,68 @@ export function nostrLoginFeature(ctx) {
} catch (e) { fail(e); }
}
+ // Reveal the seed-derived nostr key. A self-backup is published FIRST so
+ // the promise on the reveal screen is true: pasting this nsec into any
+ // coinos login finds the backup and opens THIS wallet, instead of
+ // key-deriving a fresh empty one (walletForSigner checks relays before
+ // deriving).
+ async function exportWalletKey() {
+ ui.nostrLoginError = '';
+ busy(true);
+ try {
+ const sk = wallet.nostr && wallet.nostr.sk;
+ if (!sk || !wallet.mnemonic) throw new Error(t('nlNeedSeed'));
+ await publishWalletBackup(keySigner(sk), { mnemonic: wallet.mnemonic, passphrase: wallet.passphrase || '' });
+ ui.nostrExportStep = 'shown';
+ busy(false);
+ } catch (e) { fail(e); }
+ }
+
+ // The built-in key, behind the same two-step gate as the recovery phrase:
+ // a warning you have to act on before anything secret is in the DOM.
+ function exportSection() {
+ const sk = wallet.nostr && wallet.nostr.sk;
+ if (!sk) return null;
+ if (ui.nostrExportStep === 'shown') {
+ const nsec = nsecOf(sk);
+ return h('div', { class: 'col', style: 'gap:8px' },
+ h('div', { class: 'addr-box break' }, nsec),
+ h('p', { class: 'small faint', style: 'margin:0' }, t('nlExportedNote')),
+ h('div', { class: 'row gap6' },
+ copyBtn(nsec, t('copy')),
+ h('button', { class: 'btn-sm grow', onClick: () => { ui.nostrExportStep = false; render(); } }, t('hide'))));
+ }
+ if (ui.nostrExportStep === 'warn') {
+ return h('div', { class: 'col', style: 'gap:8px' },
+ h('div', { class: 'warn-box' }, t('nlExportWarn')),
+ ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null,
+ h('div', { class: 'row gap6' },
+ h('button', { class: 'btn-primary grow', disabled: ui.nostrLoginBusy, onClick: exportWalletKey },
+ ui.nostrLoginBusy ? h('span', { class: 'spinner' }) : t('nlExportReveal')),
+ h('button', { class: 'btn-sm', onClick: () => { ui.nostrExportStep = false; ui.nostrLoginError = ''; render(); } }, t('cancel'))));
+ }
+ return h('button', {
+ class: 'btn-block', disabled: ui.nostrLoginBusy,
+ onClick: () => { ui.nostrExportStep = 'warn'; ui.nostrLoginError = ''; render(); },
+ }, t('nlExportBtn'));
+ }
+
+ function linkSection() {
+ if (!ui.nostrLinkOpen) {
+ return h('button', {
+ class: 'btn-block', disabled: ui.nostrLoginBusy,
+ onClick: () => { ui.nostrLinkOpen = true; ui.nostrLoginError = ''; render(); },
+ }, t('nlChangeBtn'));
+ }
+ return h('div', { class: 'col', style: 'gap:10px' },
+ h('div', { class: 'row between' },
+ h('strong', { class: 'small' }, t('nlChangeBtn')),
+ h('span', { class: 'linklike small', onClick: () => { ui.nostrLinkOpen = false; ui.nostrLoginError = ''; render(); } }, t('cancel'))),
+ h('p', { class: 'small muted', style: 'margin:0' }, t('nlLinkDesc')),
+ h('div', { class: 'notice info small' }, t('nlBackupWarning')),
+ signerButtons(linkOpenWallet));
+ }
+
function settingsCard() {
if (wallet.watchOnly || !wallet.mnemonic) return null;
const st = load();
@@ -306,11 +368,18 @@ export function nostrLoginFeature(ctx) {
h('p', { class: 'small muted', style: 'margin:0' }, t('nlReconnectDesc')),
signerButtons(reconnectSigner)));
}
+ // No external account linked — but the wallet still HAS a nostr account:
+ // the one derived from its seed, already signing chat and holding the
+ // payment address. A bare login form here read as "no account yet", so
+ // the built-in identity comes first and both exporting its key and
+ // switching to another account are explicit steps behind buttons.
+ const npub = wallet.nostrNpub && wallet.nostrNpub();
return h('div', { class: 'card col' },
h('h3', {}, t('nlLinkTitle')),
- h('p', { class: 'small muted', style: 'margin:0' }, t('nlLinkDesc')),
- h('div', { class: 'notice info small' }, t('nlBackupWarning')),
- signerButtons(linkOpenWallet));
+ npub ? h('div', { class: 'small muted break' }, npub) : null,
+ h('p', { class: 'small muted', style: 'margin:0' }, t('nlBuiltinDesc')),
+ exportSection(),
+ linkSection());
}
return {