Merge pull request #2007 from PR0M3TH3AN/ai/refactor-profile-modal-20231026-3866161108178541875

Refactor handleAddProfile to ProfileModalController
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-31 10:18:13 -05:00
commit cffbe26c04
5 files changed
+322 -156

No files matched your search

-147
View File
@@ -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()
-1
View File
@@ -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) =>
+142 -8
View File
@@ -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();
+2
View File
@@ -256,6 +256,8 @@ test(
const decryptCalls = [];
window.nostr = {
enable: async () => {},
getPublicKey: async () => pubkey,
nip04: {
decrypt: async () => {
decryptCalls.push("nip04");
@@ -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 = `
<div id="profileModal">
<div class="bv-modal__panel"></div>
<button id="profileAddAccountBtn" type="button"></button>
<div id="profileSwitcherList"></div>
<div id="profileModerationSettings">
<label>
<span class="text-xs"></span>
<input id="profileModerationMuteHideThreshold" type="number" />
</label>
</div>
</div>
`;
let dom;
let documentRef;
let windowRef;
let controller;
let savedProfiles;
let profileCache;
let persistedProfiles;
let loadProfileError = null;
beforeEach(async () => {
dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
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);
});