aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-18 08:27:27 -0700
committerAdam Soltys <[email protected]>2026-08-18 08:27:27 -0700
commit1f8071e5d86104f5ad86d7d8f94f32daeda7fc76 (patch)
treee89c7b3b0bbcc797414104a63165a23276177e2d /src
parentf684cee551fc2d2470f518332597dd9072a7da90 (diff)
downloadcoinosv3-1f8071e5d86104f5ad86d7d8f94f32daeda7fc76.tar.gz
coinosv3-1f8071e5d86104f5ad86d7d8f94f32daeda7fc76.zip
A missing signer opens a door instead of a toast: the reconnect screen offers the extension, the signer-app link, or a pasted key — same account enforced — and the silent reattach closes it on its own when it lands
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
Diffstat (limited to 'src')
-rw-r--r--src/app.js1
-rw-r--r--src/features/hats.js12
-rw-r--r--src/features/messages.js7
-rw-r--r--src/features/nostrlogin.js42
-rw-r--r--src/i18n.js2
5 files changed, 61 insertions, 3 deletions
diff --git a/src/app.js b/src/app.js
index fd536e1..9c793ab 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2268,6 +2268,7 @@ function goHome() {
ui.arkExitPage = null;
ui.settingsPage = null;
ui.logoutConfirm = null;
+ ui.nostrReconnect = null;
ui.addrScan = false;
ui.arkMoveOpen = false;
ui.nameEditOpen = null;
diff --git a/src/features/hats.js b/src/features/hats.js
index 4f6e977..6516459 100644
--- a/src/features/hats.js
+++ b/src/features/hats.js
@@ -230,7 +230,11 @@ export function hatsFeature(ctx) {
render();
try {
const id = await identity();
- if (!id) throw new Error(t('msgNoIdentity'));
+ if (!id) {
+ if (!hook('nostrReconnectPrompt')) toast(t('msgNoIdentity'));
+ ui.hatBuying = null; ui.hatConfirm = null; render();
+ return;
+ }
const inv = await post('/hats/invoice', { hat: hat.id, net: hatNet() }, id.signer);
await hook('arkPayInvoice', inv.invoice, { maxAmountSat: inv.sat });
const rec = await post('/hats/claim', { paymentHash: inv.paymentHash, hat: hat.id, net: hatNet() }, id.signer);
@@ -252,7 +256,11 @@ export function hatsFeature(ctx) {
render();
try {
const id = await identity();
- if (!id) throw new Error(t('msgNoIdentity'));
+ if (!id) {
+ if (!hook('nostrReconnectPrompt')) toast(t('msgNoIdentity'));
+ ui.hatBuying = null; render();
+ return;
+ }
const rec = await post('/hats/equip', { hat: hatId, net: hatNet() }, id.signer);
ui.hatShopData = { ...ui.hatShopData, owned: rec.owned, equipped: rec.equipped };
worn.set(hatNet() + ':' + id.pubkey, { hat: rec.equipped, t: Date.now() });
diff --git a/src/features/messages.js b/src/features/messages.js
index 4202a75..02affc9 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -155,7 +155,12 @@ export function messagesFeature(ctx) {
const isMe = (pk) => myPubkeys().includes(pk);
// "No identity" while soft-locked means the keys left with the lock — say
// that, not "signer disconnected" (which reads as a nostr-login problem).
- const noIdToast = () => toast(t(wallet.watchOnly ? 'msgLockedChat' : 'msgNoIdentity'));
+ const noIdToast = () => {
+ // A missing signer gets the reconnect screen, not a dead-end toast —
+ // logging out and back in was the workaround nobody should need.
+ if (!wallet.watchOnly && hook('nostrReconnectPrompt')) return;
+ toast(t(wallet.watchOnly ? 'msgLockedChat' : 'msgNoIdentity'));
+ };
// ---- shared runtime -----------------------------------------------------
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 6c06fc8..5195778 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -389,9 +389,51 @@ export function nostrLoginFeature(ctx) {
linkSection());
}
+ // The reconnect takeover: opened by any feature whose action just found
+ // the signer missing (a reload dropped it; Amethyst and friends can't
+ // reattach silently). Same guarded buttons as the settings card — an
+ // extension, the signer-app deeplink/QR, or a pasted key — and the run
+ // refuses a different account.
+ function reconnectScreen() {
+ const st = load();
+ const run = (makeSigner) => reconnectSigner(makeSigner).then(() => {
+ if (live) { ui.nostrReconnect = null; render(); }
+ });
+ return h('div', { class: 'col', style: 'gap:16px' },
+ ctx.brandHeader(false),
+ h('div', { class: 'card col', style: 'gap:12px' },
+ h('h3', { style: 'margin:0' }, t('nlReconnectTitle')),
+ h('p', { class: 'small muted', style: 'margin:0' }, t('nlReconnectBody', { npub: (npubOf(st.pubkey) || '').slice(0, 12) })),
+ signerButtons(run),
+ ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null),
+ h('button', { class: 'btn-ghost btn-block', onClick: () => {
+ stopNostrConnect();
+ ui.nostrReconnect = null; ui.nostrConnectOpen = false; ui.nostrLoginError = '';
+ render();
+ } }, t('back')));
+ }
+
return {
id: 'nostrlogin',
stop() { stopNostrConnect(); },
+ screenView() {
+ if (ui.screen !== 'wallet' || !ui.nostrReconnect) return null;
+ return reconnectScreen();
+ },
+ // An action needed the login signer and it isn't there: open the
+ // reconnect screen (and try the silent extension reattach in the
+ // background — it closes the screen by itself when it lands).
+ nostrReconnectPrompt() {
+ const st = load();
+ if (!st.pubkey || live) return false;
+ ui.nostrReconnect = true;
+ ui.nostrLoginError = '';
+ this.nostrLoginResume().then((sg) => {
+ if (sg && ui.nostrReconnect) { ui.nostrReconnect = null; render(); }
+ }).catch(() => {});
+ render();
+ return true;
+ },
init() {
// A live signer is bound to the wallet that linked it. Switching to an
// account that never did must not inherit the login identity — it used
diff --git a/src/i18n.js b/src/i18n.js
index 14e6d14..5f8f478 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -649,6 +649,8 @@ const STR = {
nlConnected: 'connected',
nlDisconnected: 'not connected',
nlReconnectDesc: "Your signer isn't connected, so this app can't sign as you. Reconnect it to post and update your profile — your wallet and balance aren't affected.",
+ nlReconnectTitle: 'Reconnect your signer',
+ nlReconnectBody: 'Your Nostr signer isn’t connected on this device. Reconnect to keep acting as {npub}…',
nlWrongAccount: 'That signer is a different Nostr account. Reconnect with {npub}.',
nlLinked: 'Wallet linked to your Nostr account',
nlNeedSeed: 'This wallet has no recovery phrase loaded.',