From ee09b723aff324d9cc7f240d4a6a069cb6c92ef8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:14:52 -0400 Subject: [PATCH] Add profile switching support for NIP-07 accounts --- js/app.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ js/nostr.js | 42 +++++++++++++++++- 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/js/app.js b/js/app.js index 3982afa1..ae25716d 100644 --- a/js/app.js +++ b/js/app.js @@ -3897,6 +3897,16 @@ class bitvidApp { }); } + if ( + this.profileAddAccountBtn && + this.profileAddAccountBtn.dataset.bound !== "true" + ) { + this.profileAddAccountBtn.dataset.bound = "true"; + this.profileAddAccountBtn.addEventListener("click", () => { + this.handleAddProfile(); + }); + } + Object.entries(this.profileNavButtons).forEach(([name, button]) => { if (!button || button.dataset.navBound === "true") { return; @@ -4440,6 +4450,119 @@ class bitvidApp { }); } + async handleAddProfile() { + if (!this.profileAddAccountBtn) { + return; + } + + const button = this.profileAddAccountBtn; + if (button.dataset.loading === "true") { + return; + } + + const titleEl = button.querySelector(".profile-switcher__addTitle"); + const hintEl = button.querySelector(".profile-switcher__addHint"); + const originalTitle = titleEl ? titleEl.textContent : ""; + const originalHint = hintEl ? hintEl.textContent : ""; + const originalAriaLabel = button.getAttribute("aria-label"); + const originalDisabled = button.disabled; + + const setLoadingState = (isLoading) => { + button.disabled = isLoading ? true : originalDisabled; + button.dataset.loading = isLoading ? "true" : "false"; + button.setAttribute("aria-busy", isLoading ? "true" : "false"); + if (isLoading) { + button.setAttribute("aria-disabled", "true"); + } else if (originalDisabled) { + button.setAttribute("aria-disabled", "true"); + } else { + button.removeAttribute("aria-disabled"); + } + if (isLoading) { + if (titleEl) { + titleEl.textContent = "Connecting..."; + } + if (hintEl) { + hintEl.textContent = "Check your extension"; + } + button.setAttribute( + "aria-label", + "Connecting to your Nostr extension" + ); + } else { + if (titleEl) { + titleEl.textContent = originalTitle; + } + if (hintEl) { + hintEl.textContent = originalHint; + } + if (originalAriaLabel === null) { + button.removeAttribute("aria-label"); + } else { + button.setAttribute("aria-label", originalAriaLabel); + } + } + }; + + setLoadingState(true); + + try { + const pubkey = await nostrClient.login({ + allowAccountSelection: true, + }); + + const normalizedPubkey = this.normalizeHexPubkey(pubkey); + if (!normalizedPubkey) { + throw new Error( + "Received an invalid public key from the Nostr extension." + ); + } + + const alreadySaved = this.savedProfiles.some( + (entry) => + this.normalizeHexPubkey(entry.pubkey) === normalizedPubkey + ); + if (alreadySaved) { + this.showSuccess("That profile is already saved on this device."); + return; + } + + const npub = this.safeEncodeNpub(normalizedPubkey) || ""; + let profileMeta = this.getProfileCacheEntry(normalizedPubkey)?.profile; + + if (!profileMeta) { + await this.loadOwnProfile(normalizedPubkey); + profileMeta = this.getProfileCacheEntry(normalizedPubkey)?.profile; + } + + const name = profileMeta?.name || ""; + const picture = + profileMeta?.picture || "assets/svg/default-profile.svg"; + + this.savedProfiles.push({ + pubkey: normalizedPubkey, + npub, + name, + picture, + authType: "nip07", + }); + + this.persistSavedProfiles({ persistActive: false }); + this.renderSavedProfiles(); + + this.showSuccess("Profile added. Select it when you're ready to switch."); + } catch (error) { + console.error("Failed to add profile via NIP-07:", error); + const message = + error && typeof error.message === "string" && error.message.trim() + ? error.message.trim() + : "Couldn't add that profile. Please try again."; + this.showError(message); + } finally { + setLoadingState(false); + } + } + async handleAddBlockedCreator() { if (!this.profileBlockedInput) { return; diff --git a/js/nostr.js b/js/nostr.js index 2de5080a..126658c0 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -593,7 +593,7 @@ class NostrClient { /** * Attempt login with a Nostr extension */ - async login() { + async login(options = {}) { try { const extension = window.nostr; if (!extension) { @@ -603,6 +603,13 @@ class NostrClient { ); } + const { allowAccountSelection = false, expectPubkey } = + typeof options === "object" && options !== null ? options : {}; + const normalizedExpectedPubkey = + typeof expectPubkey === "string" && expectPubkey.trim() + ? expectPubkey.trim().toLowerCase() + : null; + if (typeof extension.getPublicKey !== "function") { throw new Error( "This NIP-07 extension is missing getPublicKey support. Please update the extension." @@ -624,6 +631,30 @@ class NostrClient { } } + if (allowAccountSelection && typeof extension.selectAccounts === "function") { + try { + const selection = await extension.selectAccounts( + expectPubkey ? [expectPubkey] : undefined + ); + + const didCancelSelection = + selection === undefined || + selection === null || + selection === false || + (Array.isArray(selection) && selection.length === 0); + + if (didCancelSelection) { + throw new Error("Account selection was cancelled."); + } + } catch (selectionErr) { + const message = + selectionErr && typeof selectionErr.message === "string" + ? selectionErr.message + : "Account selection was cancelled."; + throw new Error(message); + } + } + let timeoutId; const pubkey = await Promise.race([ extension.getPublicKey(), @@ -646,6 +677,15 @@ class NostrClient { "The NIP-07 extension did not return a public key. Please try again." ); } + + if ( + normalizedExpectedPubkey && + pubkey.toLowerCase() !== normalizedExpectedPubkey + ) { + throw new Error( + "The selected account doesn't match the expected profile. Please try again." + ); + } const npub = window.NostrTools.nip19.npubEncode(pubkey); if (isDevMode) {