From 13ab4e790de9f19c7491edebf6b5067daf9bece0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:38:26 +0000 Subject: [PATCH] Fix: Cancel NIP-46 handshake on login modal cancellation Implemented a mechanism to cancel the pending NIP-46 handshake when the user clicks 'Cancel' in the login modal. - Added `pendingHandshakeCancel` callback to `NostrClient` to reject the handshake promise. - Updated `LoginModalController` to invoke `disconnectRemoteSigner` on cancellation. - Prevented `onLoginError` callback from triggering when the error code is `login-cancelled`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/nostr/client.js | 23 +++++++++++++++++++++++ js/ui/loginModalController.js | 8 ++++++++ 2 files changed, 31 insertions(+) diff --git a/js/nostr/client.js b/js/nostr/client.js index 05511947..b753ccd7 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -1263,6 +1263,7 @@ export class NostrClient { this.unreachableRelays = new Set(); this.isInitialized = false; this.nip46Client = null; + this.pendingHandshakeCancel = null; this.remoteSignerListeners = new Set(); this.sessionActorListeners = new Set(); const storedRemoteSigner = this.getStoredNip46Metadata(); @@ -1708,6 +1709,9 @@ export class NostrClient { return; } settled = true; + if (this.pendingHandshakeCancel === cancelHandshake) { + this.pendingHandshakeCancel = null; + } try { subscription?.unsub?.(); } catch (error) { @@ -1718,6 +1722,15 @@ export class NostrClient { } }; + const cancelHandshake = () => { + cleanup(); + const error = new Error("Remote signer connection cancelled."); + error.code = "login-cancelled"; + reject(error); + }; + + this.pendingHandshakeCancel = cancelHandshake; + const timeoutId = setTimeout(() => { cleanup(); const error = new Error("Timed out waiting for the remote signer to acknowledge the connection."); @@ -2438,6 +2451,16 @@ export class NostrClient { */ async disconnectRemoteSigner({ keepStored = true } = {}) { this.pendingRemoteSignerRestore = null; + + if (this.pendingHandshakeCancel) { + try { + this.pendingHandshakeCancel(); + } catch (error) { + devLogger.warn("[nostr] Failed to cancel pending handshake:", error); + } + this.pendingHandshakeCancel = null; + } + if (this.nip46Client) { try { await this.nip46Client.destroy(); diff --git a/js/ui/loginModalController.js b/js/ui/loginModalController.js index 135007cf..bc323ecd 100644 --- a/js/ui/loginModalController.js +++ b/js/ui/loginModalController.js @@ -1487,6 +1487,7 @@ export default class LoginModalController { this.rejectPendingTask(this.createCancellationError(), { type: "add-profile", }); + this.handleProviderDisconnect("nip46"); cleanup(); } else { finish(null, { keepMounted: false }); @@ -2392,6 +2393,13 @@ export default class LoginModalController { return; } + if ( + rejectionError.code === "login-cancelled" || + rejectionError.code === "user-cancelled" + ) { + return; + } + await safeInvokeAsync(this.callbacks.onLoginError, { provider: entry.provider.source || entry.provider, error: rejectionError,