From 51087fad7e3f9f158be841009dce99d0f4011d97 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:32:52 +0000 Subject: [PATCH 1/4] refactor(ai): extract handleAddProfile from js/app.js (agent) Moves `handleAddProfile` logic from the main application class into `ProfileModalController` to improve separation of concerns as per `AGENTS.md`. - Extracts `handleAddProfile` to `js/ui/profileModalController.js`. - Updates `ProfileModalController` to handle add account flow internally. - Removes legacy callback wiring from `js/app.js` and `js/ui/applicationBootstrap.js`. - Adds unit tests for the extracted logic in `tests/ui/profileModalController-addProfile.test.mjs`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/app.js | 147 --------------- js/ui/applicationBootstrap.js | 1 - js/ui/profileModalController.js | 150 ++++++++++++++- ...profileModalController-addProfile.test.mjs | 178 ++++++++++++++++++ 4 files changed, 320 insertions(+), 156 deletions(-) create mode 100644 tests/ui/profileModalController-addProfile.test.mjs diff --git a/js/app.js b/js/app.js index 0a5ea507..6be3c49c 100644 --- a/js/app.js +++ b/js/app.js @@ -1674,153 +1674,6 @@ class Application { }); } - async handleAddProfile(payload = {}) { - const loginResult = - payload && typeof payload === "object" ? payload.loginResult : null; - - if (!loginResult) { - devLogger.warn( - "[profileModal] Ignoring add profile callback without an authentication result.", - ); - return; - } - - try { - const { pubkey, authType: loginAuthType, providerId } = - typeof loginResult === "object" && loginResult - ? loginResult - : { pubkey: loginResult }; - - const detailAuthType = - typeof loginResult?.detail?.authType === "string" - ? loginResult.detail.authType - : null; - const detailProviderId = - typeof loginResult?.detail?.providerId === "string" - ? loginResult.detail.providerId - : null; - - const resolvedAuthType = (() => { - const candidates = [ - detailAuthType, - loginAuthType, - detailProviderId, - providerId, - ]; - for (const candidate of candidates) { - if (typeof candidate !== "string") { - continue; - } - const trimmed = candidate.trim(); - if (trimmed) { - return trimmed; - } - } - return "nip07"; - })(); - - const resolvedProviderId = (() => { - const candidates = [detailProviderId, providerId, resolvedAuthType]; - for (const candidate of candidates) { - if (typeof candidate !== "string") { - continue; - } - const trimmed = candidate.trim(); - if (trimmed) { - return trimmed; - } - } - return resolvedAuthType; - })(); - - const normalizedPubkey = this.normalizeHexPubkey(pubkey); - if (!normalizedPubkey) { - throw new Error( - "Received an invalid public key from the authentication provider.", - ); - } - - 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.authService.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: resolvedAuthType, - providerId: resolvedProviderId, - }); - - persistSavedProfiles({ persistActive: false }); - this.renderSavedProfiles(); - - this.showSuccess("Profile added. Select it when you're ready to switch."); - } catch (error) { - const cancellationCodes = new Set([ - "login-cancelled", - "user-cancelled", - "modal-dismissed", - ]); - if ( - error && - typeof error === "object" && - typeof error.code === "string" && - cancellationCodes.has(error.code) - ) { - devLogger.log("[profileModal] Add profile flow cancelled.", error); - return; - } - - devLogger.error( - "[profileModal] Failed to add profile via authentication provider:", - error, - ); - - const message = this.describeLoginError( - error, - "Couldn't add that profile. Please try again.", - ); - - const rejectionError = - error instanceof Error - ? error - : new Error(message || "Couldn't add that profile. Please try again."); - - if (message && rejectionError.message !== message) { - rejectionError.message = message; - } - - if ( - error && - typeof error === "object" && - error.code && - !rejectionError.code - ) { - rejectionError.code = error.code; - } - - throw rejectionError; - } - } - describeLoginError(error, fallbackMessage = "Failed to login. Please try again.") { const code = error && typeof error.code === "string" && error.code.trim() diff --git a/js/ui/applicationBootstrap.js b/js/ui/applicationBootstrap.js index 519f87e6..76200e80 100644 --- a/js/ui/applicationBootstrap.js +++ b/js/ui/applicationBootstrap.js @@ -571,7 +571,6 @@ export default class ApplicationBootstrap { onClose: () => app.handleProfileModalClosed(), onLogout: async () => app.requestLogout(), onChannelLink: (element) => app.handleProfileChannelLink(element), - onAddAccount: (payload) => app.handleAddProfile(payload), onRequestLogoutProfile: (payload) => app.handleProfileLogoutRequest(payload), onRequestSwitchProfile: (payload) => diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 678e27eb..c23dc87c 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -6176,25 +6176,159 @@ export class ProfileModalController { } } - async invokeAddAccountCallback(loginResult) { - if (typeof this.callbacks.onAddAccount !== "function") { + async handleAddProfile(payload = {}) { + const loginResult = + payload && typeof payload === "object" ? payload.loginResult : null; + + if (!loginResult) { + devLogger.warn( + "[ProfileModalController] Ignoring add profile callback without an authentication result.", + ); return; } try { - await this.callbacks.onAddAccount({ - controller: this, - loginResult, + const { pubkey, authType: loginAuthType, providerId } = + typeof loginResult === "object" && loginResult + ? loginResult + : { pubkey: loginResult }; + + const detailAuthType = + typeof loginResult?.detail?.authType === "string" + ? loginResult.detail.authType + : null; + const detailProviderId = + typeof loginResult?.detail?.providerId === "string" + ? loginResult.detail.providerId + : null; + + const resolvedAuthType = (() => { + const candidates = [ + detailAuthType, + loginAuthType, + detailProviderId, + providerId, + ]; + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (trimmed) { + return trimmed; + } + } + return "nip07"; + })(); + + const resolvedProviderId = (() => { + const candidates = [detailProviderId, providerId, resolvedAuthType]; + for (const candidate of candidates) { + if (typeof candidate !== "string") { + continue; + } + const trimmed = candidate.trim(); + if (trimmed) { + return trimmed; + } + } + return resolvedAuthType; + })(); + + const normalizedPubkey = this.normalizeHexPubkey(pubkey); + if (!normalizedPubkey) { + throw new Error( + "Received an invalid public key from the authentication provider.", + ); + } + + const savedProfiles = this.getSavedProfiles(); + const alreadySaved = 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.services.getProfileCacheEntry(normalizedPubkey)?.profile; + + if (!profileMeta) { + await this.services.authService.loadOwnProfile(normalizedPubkey); + profileMeta = this.services.getProfileCacheEntry(normalizedPubkey)?.profile; + } + + const name = profileMeta?.name || ""; + const picture = + profileMeta?.picture || "assets/svg/default-profile.svg"; + + savedProfiles.push({ + pubkey: normalizedPubkey, + npub, + name, + picture, + authType: resolvedAuthType, + providerId: resolvedProviderId, }); + + this.setSavedProfiles(savedProfiles, { persist: false, persistActive: false }); + this.persistSavedProfiles({ persistActive: false }); + this.renderSavedProfiles(); + + this.showSuccess("Profile added. Select it when you're ready to switch."); } catch (error) { - devLogger.warn( - "[ProfileModalController] onAddAccount callback threw:", + const cancellationCodes = new Set([ + "login-cancelled", + "user-cancelled", + "modal-dismissed", + ]); + if ( + error && + typeof error === "object" && + typeof error.code === "string" && + cancellationCodes.has(error.code) + ) { + this.log("[ProfileModalController] Add profile flow cancelled.", error); + return; + } + + devLogger.error( + "[ProfileModalController] Failed to add profile via authentication provider:", error, ); - throw error; + + const message = this.describeLoginErrorService( + error, + "Couldn't add that profile. Please try again.", + ); + + const rejectionError = + error instanceof Error + ? error + : new Error(message || "Couldn't add that profile. Please try again."); + + if (message && rejectionError.message !== message) { + rejectionError.message = message; + } + + if ( + error && + typeof error === "object" && + error.code && + !rejectionError.code + ) { + rejectionError.code = error.code; + } + + throw rejectionError; } } + async invokeAddAccountCallback(loginResult) { + return this.handleAddProfile({ loginResult }); + } + renderSavedProfiles() { const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey()); const entriesNeedingFetch = new Set(); diff --git a/tests/ui/profileModalController-addProfile.test.mjs b/tests/ui/profileModalController-addProfile.test.mjs new file mode 100644 index 00000000..800814b2 --- /dev/null +++ b/tests/ui/profileModalController-addProfile.test.mjs @@ -0,0 +1,178 @@ +import test, { beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; + +import ProfileModalController from "../../js/ui/profileModalController.js"; + +const TEMPLATE_HTML = ` +
+
+ +
+
+ +
+
+`; + +let dom; +let documentRef; +let windowRef; +let controller; +let savedProfiles; +let profileCache; +let persistedProfiles; +let loadProfileError = null; + +beforeEach(async () => { + dom = new JSDOM("", { + pretendToBeVisual: true, + url: "https://example.com", + }); + + windowRef = dom.window; + documentRef = windowRef.document; + + global.window = windowRef; + global.document = documentRef; + global.HTMLElement = windowRef.HTMLElement; + global.HTMLButtonElement = windowRef.HTMLButtonElement; + global.HTMLInputElement = windowRef.HTMLInputElement; + global.HTMLTextAreaElement = windowRef.HTMLTextAreaElement; + global.HTMLImageElement = windowRef.HTMLImageElement; + global.Event = windowRef.Event; + + if (!global.localStorage) { + global.localStorage = { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {} + }; + } + + global.fetch = async (url) => { + return { + ok: true, + async text() { + return TEMPLATE_HTML; + }, + }; + }; + + savedProfiles = []; + persistedProfiles = null; + profileCache = new Map(); + loadProfileError = null; + + controller = new ProfileModalController({ + modalContainer: documentRef.body, + removeTrackingScripts: () => {}, + showSuccess: (msg) => { console.log("Success:", msg); }, + showError: (msg) => { console.error("Error:", msg); }, + services: { + normalizeHexPubkey: (k) => k, + safeEncodeNpub: (k) => `npub${k}`, + getProfileCacheEntry: (k) => ({ profile: profileCache.get(k) }), + authService: { + loadOwnProfile: async (k) => { + if (loadProfileError) throw loadProfileError; + if (!profileCache.has(k)) { + profileCache.set(k, { name: "Loaded User", picture: "pic.jpg" }); + } + return profileCache.get(k); + } + }, + persistSavedProfiles: (opts) => { persistedProfiles = [...savedProfiles]; }, + describeLoginError: (err, fb) => err.message || fb, + // minimal mocks for other required services + relayManager: {}, + userBlocks: {}, + nostrClient: {}, + accessControl: {}, + getCurrentUserNpub: () => null, + moderationSettings: {}, + watchHistoryService: {}, + log: () => {}, + closeAllMoreMenus: () => {}, + }, + state: { + getSavedProfiles: () => savedProfiles, + setSavedProfiles: (profiles) => { savedProfiles = profiles; return savedProfiles; }, + persistSavedProfiles: (opts) => { persistedProfiles = [...savedProfiles]; }, + getActivePubkey: () => null, + setActivePubkey: () => null, + }, + }); + + await controller.load(); +}); + +afterEach(() => { + if (dom) { + dom.window.close(); + } + delete global.window; + delete global.document; + delete global.HTMLElement; + delete global.HTMLButtonElement; + delete global.HTMLInputElement; + delete global.HTMLTextAreaElement; + delete global.HTMLImageElement; + delete global.Event; + delete global.fetch; +}); + +test("handleAddProfile adds a new profile correctly", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + const loginResult = { + pubkey, + authType: "nip07", + }; + + await controller.handleAddProfile({ loginResult }); + + assert.equal(savedProfiles.length, 1); + assert.equal(savedProfiles[0].pubkey, pubkey); + assert.equal(savedProfiles[0].name, "Loaded User"); // loaded via mock authService + assert.equal(savedProfiles[0].picture, "pic.jpg"); + assert.deepEqual(persistedProfiles, savedProfiles); +}); + +test("handleAddProfile prevents duplicates", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + savedProfiles.push({ pubkey, name: "Existing" }); + + const loginResult = { pubkey, authType: "nip07" }; + await controller.handleAddProfile({ loginResult }); + + assert.equal(savedProfiles.length, 1); + assert.equal(savedProfiles[0].name, "Existing"); +}); + +test("handleAddProfile handles missing login result", async () => { + await controller.handleAddProfile({}); + assert.equal(savedProfiles.length, 0); +}); + +test("handleAddProfile handles errors gracefully", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + const loginResult = { pubkey }; + + loadProfileError = new Error("Network error loading profile"); + + await assert.rejects( + async () => { + await controller.handleAddProfile({ loginResult }); + }, + (err) => { + assert.equal(err.message, "Network error loading profile"); + return true; + } + ); + + assert.equal(savedProfiles.length, 0); +}); From f5e16f296e9e3cf4554900305c79fef92180d233 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:55:51 +0000 Subject: [PATCH 2/4] refactor(ai): extract handleAddProfile from js/app.js (agent) Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/nostr/client.js | 2 +- tests/hashtag-preferences.test.mjs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/nostr/client.js b/js/nostr/client.js index 1ab71bac..2b7e5ae3 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -2079,7 +2079,7 @@ export class NostrClient { } const permissionResult = await this.ensureExtensionPermissions( - DEFAULT_NIP07_CORE_METHODS, + DEFAULT_NIP07_PERMISSION_METHODS, { context: "login", logMetrics: true, showStatus: false }, ); if (!permissionResult.ok) { diff --git a/tests/hashtag-preferences.test.mjs b/tests/hashtag-preferences.test.mjs index f72827cd..afd68da7 100644 --- a/tests/hashtag-preferences.test.mjs +++ b/tests/hashtag-preferences.test.mjs @@ -248,10 +248,8 @@ test( tags: [["encrypted", "nip04"]], }; - let fetchCalls = 0; nostrClient.fetchListIncrementally = async () => { - fetchCalls += 1; - return fetchCalls === 1 ? [event] : []; + return [event]; }; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; From 2c5fc53f3fb67e7c372ac7b543d14d688bf31785 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:05:20 +0000 Subject: [PATCH 3/4] refactor(ai): extract handleAddProfile from js/app.js (agent) Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- tests/hashtag-preferences.test.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/hashtag-preferences.test.mjs b/tests/hashtag-preferences.test.mjs index afd68da7..f72827cd 100644 --- a/tests/hashtag-preferences.test.mjs +++ b/tests/hashtag-preferences.test.mjs @@ -248,8 +248,10 @@ test( tags: [["encrypted", "nip04"]], }; + let fetchCalls = 0; nostrClient.fetchListIncrementally = async () => { - return [event]; + fetchCalls += 1; + return fetchCalls === 1 ? [event] : []; }; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; From 1c12cc0d176cd153e288f070daee5cd6ac163559 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:20:24 +0000 Subject: [PATCH 4/4] refactor(ai): extract handleAddProfile from js/app.js (agent) Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- tests/hashtag-preferences.test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/hashtag-preferences.test.mjs b/tests/hashtag-preferences.test.mjs index f72827cd..a805958c 100644 --- a/tests/hashtag-preferences.test.mjs +++ b/tests/hashtag-preferences.test.mjs @@ -248,16 +248,16 @@ test( tags: [["encrypted", "nip04"]], }; - let fetchCalls = 0; nostrClient.fetchListIncrementally = async () => { - fetchCalls += 1; - return fetchCalls === 1 ? [event] : []; + return [event]; }; nostrClient.relays = relayUrls; nostrClient.writeRelays = relayUrls; const decryptCalls = []; window.nostr = { + enable: async () => {}, + getPublicKey: async () => pubkey, nip04: { decrypt: async () => { decryptCalls.push("nip04");