mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 00:12:25 +00:00
Merge pull request #2007 from PR0M3TH3AN/ai/refactor-profile-modal-20231026-3866161108178541875
Refactor handleAddProfile to ProfileModalController
This commit is contained in:
5 files changed
+322
-156
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user