Merge pull request #818 from PR0M3TH3AN/codex/refactor-login-modal-and-profile-handling

Refactor profile add flow to use login modal controller
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-22 22:39:57 -04:00
commit 57ebf71d42
3 files changed
+680 -260

No files matched your search

+65 -254
View File
@@ -415,7 +415,6 @@ class Application {
// Profile and login modal controller state
this.profileController = null;
this.loginModalController = null;
this.loginModalPendingTask = null;
this.currentUserNpub = null;
this.initializeLoginModalController({ logIfMissing: true });
@@ -567,6 +566,10 @@ class Application {
persistSavedProfiles: (options) => persistSavedProfiles(options),
watchHistoryService,
authService: this.authService,
requestAddProfileLogin: (options) =>
this.requestProfileAdditionLogin(options),
describeLoginError: (error, fallbackMessage) =>
this.describeLoginError(error, fallbackMessage),
log: (...args) => this.log(...args),
closeAllMoreMenus: (options) => this.closeAllMoreMenus(options),
clipboard:
@@ -595,7 +598,7 @@ class Application {
onClose: () => this.handleProfileModalClosed(),
onLogout: async () => this.requestLogout(),
onChannelLink: (element) => this.handleProfileChannelLink(element),
onAddAccount: (controller) => this.handleAddProfile(controller),
onAddAccount: (payload) => this.handleAddProfile(payload),
onRequestSwitchProfile: (payload) =>
this.handleProfileSwitchRequest(payload),
onRelayOperation: (payload) =>
@@ -1905,17 +1908,6 @@ class Application {
devLogger.log("[LoginModal] Login result returned pubkey:", pubkey);
const pendingTask =
this.loginModalPendingTask &&
this.loginModalPendingTask.type === "add-profile"
? this.loginModalPendingTask
: null;
if (pendingTask) {
pendingTask.resolve(result);
return;
}
if (!result || typeof result !== "object") {
return;
}
@@ -1959,36 +1951,6 @@ class Application {
? fallbackMessage.trim()
: "Failed to login. Please try again.";
const pendingTask =
this.loginModalPendingTask &&
this.loginModalPendingTask.type === "add-profile"
? this.loginModalPendingTask
: null;
if (pendingTask) {
const rejectionError =
error instanceof Error
? error
: new Error(normalizedMessage || "Failed to login.");
if (
normalizedMessage &&
rejectionError &&
normalizedMessage !== rejectionError.message
) {
rejectionError.message = normalizedMessage;
}
if (
error &&
typeof error === "object" &&
error.code &&
!rejectionError.code
) {
rejectionError.code = error.code;
}
pendingTask.reject(rejectionError);
return;
}
userLogger.warn(
provider && provider.id
? `[LoginModal] Login failed for provider ${provider.id}.`
@@ -2001,12 +1963,15 @@ class Application {
this.initializeLoginModalController();
if (
this.loginModalController &&
typeof this.loginModalController.setNextRequestLoginOptions ===
"function"
typeof this.loginModalController.requestAddProfileLogin === "function"
) {
try {
return await this.openLoginModalForProfileAddition({
return await this.loginModalController.requestAddProfileLogin({
triggerElement,
requestOptions: {
allowAccountSelection: true,
autoApply: false,
},
});
} catch (error) {
if (
@@ -2035,213 +2000,18 @@ class Application {
});
}
async openLoginModalForProfileAddition({ triggerElement } = {}) {
const controller = this.loginModalController;
if (!controller) {
const error = new Error("Login modal unavailable.");
error.code = "modal-unavailable";
throw error;
}
async handleAddProfile(payload = {}) {
const loginResult =
payload && typeof payload === "object" ? payload.loginResult : null;
if (typeof controller.setNextRequestLoginOptions === "function") {
controller.setNextRequestLoginOptions({
allowAccountSelection: true,
autoApply: false,
});
}
const loginModal =
prepareStaticModal({ id: "loginModal" }) ||
document.getElementById("loginModal");
if (!loginModal) {
const error = new Error("Login modal unavailable.");
error.code = "modal-unavailable";
throw error;
}
if (this.loginModalPendingTask) {
const error = new Error(
"Another login operation is already in progress.",
if (!loginResult) {
devLogger.warn(
"[profileModal] Ignoring add profile callback without an authentication result.",
);
error.code = "login-in-progress";
throw error;
}
const isModalOpen = () =>
loginModal.getAttribute("data-open") === "true" &&
!loginModal.classList.contains("hidden");
return new Promise((resolve, reject) => {
let observer = null;
let pollTimer = null;
let settled = false;
const pendingTask = { type: "add-profile" };
const finalize =
(handler) =>
(value) => {
if (settled) {
return;
}
settled = true;
if (observer) {
observer.disconnect();
observer = null;
}
if (
pollTimer !== null &&
typeof window !== "undefined" &&
typeof window.clearInterval === "function"
) {
window.clearInterval(pollTimer);
}
pollTimer = null;
if (this.loginModalPendingTask === pendingTask) {
this.loginModalPendingTask = null;
}
if (!isModalOpen() && typeof setGlobalModalState === "function") {
setGlobalModalState("login", false);
}
handler(value);
};
pendingTask.resolve = finalize((value) => resolve(value));
pendingTask.reject = finalize((errorValue) => reject(errorValue));
this.loginModalPendingTask = pendingTask;
if (typeof MutationObserver === "function") {
observer = new MutationObserver(() => {
if (!isModalOpen()) {
const cancelError = new Error("Login cancelled.");
cancelError.code = "login-cancelled";
pendingTask.reject(cancelError);
}
});
observer.observe(loginModal, {
attributes: true,
attributeFilter: ["data-open", "class"],
});
} else if (
typeof window !== "undefined" &&
typeof window.setInterval === "function"
) {
pollTimer = window.setInterval(() => {
if (!isModalOpen()) {
const cancelError = new Error("Login cancelled.");
cancelError.code = "login-cancelled";
pendingTask.reject(cancelError);
}
}, 200);
}
const opened = openStaticModal(loginModal, { triggerElement });
const currentlyOpen = isModalOpen();
if (!opened && !currentlyOpen) {
const openError = new Error("Unable to open login modal.");
openError.code = "modal-open-failed";
pendingTask.reject(openError);
return;
}
if (typeof setGlobalModalState === "function") {
setGlobalModalState("login", true);
}
});
}
async handleAddProfile(controller) {
const button =
(controller && controller.addAccountButton) ||
this.profileController?.addAccountButton ||
null;
if (!(button instanceof HTMLElement)) {
return;
}
if (button.dataset.state === "loading") {
return;
}
const titleEl = button.querySelector('[data-profile-add-title]');
const hintEl = button.querySelector('[data-profile-add-hint]');
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;
if (isLoading) {
button.dataset.state = "loading";
} else {
delete button.dataset.state;
}
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 = "Complete the login prompt from your provider.";
}
button.setAttribute(
"aria-label",
"Connecting to your Nostr account",
);
} 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 {
let loginResult;
try {
loginResult = await this.requestProfileAdditionLogin({
triggerElement: button,
});
} catch (requestError) {
const cancellationCodes = new Set([
"login-cancelled",
"user-cancelled",
"modal-dismissed",
]);
if (
requestError &&
typeof requestError === "object" &&
typeof requestError.code === "string" &&
cancellationCodes.has(requestError.code)
) {
devLogger.log(
"[profileModal] Add profile login cancelled by user.",
requestError,
);
return;
}
throw requestError;
}
const { pubkey, authType: loginAuthType, providerId } =
typeof loginResult === "object" && loginResult
? loginResult
@@ -2257,7 +2027,12 @@ class Application {
: null;
const resolvedAuthType = (() => {
const candidates = [detailAuthType, loginAuthType, detailProviderId, providerId];
const candidates = [
detailAuthType,
loginAuthType,
detailProviderId,
providerId,
];
for (const candidate of candidates) {
if (typeof candidate !== "string") {
continue;
@@ -2292,8 +2067,7 @@ class Application {
}
const alreadySaved = this.savedProfiles.some(
(entry) =>
this.normalizeHexPubkey(entry.pubkey) === normalizedPubkey,
(entry) => this.normalizeHexPubkey(entry.pubkey) === normalizedPubkey,
);
if (alreadySaved) {
this.showSuccess("That profile is already saved on this device.");
@@ -2345,13 +2119,31 @@ class Application {
"[profileModal] Failed to add profile via authentication provider:",
error,
);
const message = this.describeLoginError(
error,
"Couldn't add that profile. Please try again.",
);
this.showError(message);
} finally {
setLoadingState(false);
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;
}
}
@@ -2401,12 +2193,26 @@ class Application {
return false;
}
const preparedModal =
prepareStaticModal({ root: loginModalElement }) || loginModalElement;
loginModalElement = preparedModal;
const closeLoginModal = () => {
if (closeStaticModal("loginModal")) {
if (closeStaticModal(loginModalElement)) {
setGlobalModalState("login", false);
}
};
const openLoginModal = ({ modal, triggerElement } = {}) => {
const target =
modal instanceof HTMLElement ? modal : loginModalElement;
return openStaticModal(target, { triggerElement });
};
const prepareLoginModal = (modal) =>
prepareStaticModal({ root: modal || loginModalElement });
try {
this.loginModalController = new LoginModalController({
modalElement: loginModalElement,
@@ -2444,6 +2250,11 @@ class Application {
},
helpers: {
closeModal: closeLoginModal,
openModal: ({ modal, triggerElement } = {}) =>
openLoginModal({ modal, triggerElement }),
prepareModal: (modal) => prepareLoginModal(modal),
setModalState: (_, isOpen) =>
setGlobalModalState("login", isOpen === undefined ? false : !!isOpen),
describeLoginError: (error, fallbackMessage) =>
this.describeLoginError(
error,
+375 -5
View File
@@ -3,6 +3,15 @@ import { renderQrCode } from "../utils/qrcode.js";
const noop = () => {};
const SLOW_PROVIDER_DELAY_MS = 8_000;
const MODAL_CLOSE_POLL_INTERVAL_MS = 200;
function createError(code, message) {
const error = new Error(message);
if (code) {
error.code = code;
}
return error;
}
function toArray(input) {
if (!input) {
@@ -247,6 +256,12 @@ export default class LoginModalController {
typeof helpers.describeLoginError === "function"
? helpers.describeLoginError
: (_, fallbackMessage) => fallbackMessage,
openModal:
typeof helpers.openModal === "function" ? helpers.openModal : null,
prepareModal:
typeof helpers.prepareModal === "function" ? helpers.prepareModal : null,
setModalState:
typeof helpers.setModalState === "function" ? helpers.setModalState : null,
};
this.providerContainer = null;
@@ -267,6 +282,8 @@ export default class LoginModalController {
this.lastRemoteSignerStatus = null;
this.nextRequestLoginOptions = null;
this.nextRequestLoginOptionsResolver = null;
this.pendingTask = null;
this.modalPrepared = false;
this.initializeRemoteSignerStatus();
this.initialized = false;
@@ -299,6 +316,309 @@ export default class LoginModalController {
this.initialized = true;
}
ensureModalPrepared() {
if (!(this.modalElement instanceof HTMLElement)) {
return null;
}
if (!this.modalPrepared && typeof this.helpers.prepareModal === "function") {
try {
const prepared = this.helpers.prepareModal(this.modalElement);
if (prepared instanceof HTMLElement) {
this.modalElement = prepared;
}
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to prepare login modal for accessibility:",
error,
);
}
}
this.modalPrepared = true;
return this.modalElement instanceof HTMLElement ? this.modalElement : null;
}
isModalOpen() {
const modal = this.modalElement;
if (!(modal instanceof HTMLElement)) {
return false;
}
if (typeof modal.dataset?.open === "string") {
return modal.dataset.open === "true";
}
const attr = modal.getAttribute("data-open");
if (typeof attr === "string") {
return attr === "true";
}
return !modal.classList.contains("hidden");
}
openModal({ triggerElement } = {}) {
const modal = this.ensureModalPrepared();
if (!(modal instanceof HTMLElement)) {
throw this.createModalUnavailableError();
}
if (typeof this.helpers.openModal === "function") {
try {
const result = this.helpers.openModal({
modal,
triggerElement,
});
return result !== false;
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to open login modal via helper:",
error,
);
return false;
}
}
try {
modal.classList.remove("hidden");
modal.setAttribute("data-open", "true");
return true;
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to toggle login modal visibility:",
error,
);
}
return false;
}
createModalUnavailableError() {
return createError("modal-unavailable", "Login modal unavailable.");
}
createLoginInProgressError() {
return createError(
"login-in-progress",
"Another login operation is already in progress.",
);
}
createModalOpenFailedError() {
return createError("modal-open-failed", "Unable to open login modal.");
}
createCancellationError() {
return createError("login-cancelled", "Login cancelled.");
}
startModalCloseWatch(task) {
const modal = this.modalElement;
if (!(modal instanceof HTMLElement)) {
return;
}
const handleClose = () => {
if (task.settled) {
return;
}
task.reject(this.createCancellationError());
};
if (typeof MutationObserver === "function") {
try {
const observer = new MutationObserver(() => {
if (!this.isModalOpen()) {
handleClose();
}
});
observer.observe(modal, {
attributes: true,
attributeFilter: ["data-open", "class"],
});
task.observer = observer;
return;
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to observe login modal close state:",
error,
);
}
}
if (
this.window &&
typeof this.window.setInterval === "function" &&
typeof this.window.clearInterval === "function"
) {
const timerId = this.window.setInterval(() => {
if (!this.isModalOpen()) {
handleClose();
}
}, MODAL_CLOSE_POLL_INTERVAL_MS);
task.pollTimer = timerId;
}
}
stopModalCloseWatch(task) {
if (!task) {
return;
}
if (task.observer && typeof task.observer.disconnect === "function") {
try {
task.observer.disconnect();
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to disconnect login modal observer:",
error,
);
}
}
task.observer = null;
if (
typeof task.pollTimer === "number" &&
this.window &&
typeof this.window.clearInterval === "function"
) {
try {
this.window.clearInterval(task.pollTimer);
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to clear login modal polling timer:",
error,
);
}
}
task.pollTimer = null;
}
createPendingModalTask(type, { triggerElement } = {}) {
const modal = this.ensureModalPrepared();
if (!(modal instanceof HTMLElement)) {
throw this.createModalUnavailableError();
}
if (this.pendingTask) {
throw this.createLoginInProgressError();
}
return new Promise((resolve, reject) => {
const task = {
type,
resolve: null,
reject: null,
settled: false,
observer: null,
pollTimer: null,
};
const finalize = (handler) => (value) => {
if (task.settled) {
return;
}
task.settled = true;
this.stopModalCloseWatch(task);
if (this.pendingTask === task) {
this.pendingTask = null;
}
if (
typeof this.helpers.setModalState === "function" &&
!this.isModalOpen()
) {
try {
this.helpers.setModalState("login", false);
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to synchronize login modal state:",
error,
);
}
}
handler(value);
};
task.resolve = finalize((value) => resolve(value));
task.reject = finalize((errorValue) => reject(errorValue));
this.pendingTask = task;
this.startModalCloseWatch(task);
let opened = false;
try {
opened = this.openModal({ triggerElement });
} catch (error) {
devLogger.warn(
"[LoginModalController] Opening login modal threw:",
error,
);
}
if (!opened && !this.isModalOpen()) {
task.reject(this.createModalOpenFailedError());
return;
}
if (typeof this.helpers.setModalState === "function") {
try {
this.helpers.setModalState("login", true);
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to mark login modal as open in app state:",
error,
);
}
}
});
}
resolvePendingTask(result, { type } = {}) {
const task = this.pendingTask;
if (!task || (type && task.type !== type) || typeof task.resolve !== "function") {
return false;
}
task.resolve(result);
return true;
}
rejectPendingTask(error, { type } = {}) {
const task = this.pendingTask;
if (!task || (type && task.type !== type) || typeof task.reject !== "function") {
return false;
}
task.reject(error);
return true;
}
requestAddProfileLogin({ triggerElement, requestOptions } = {}) {
const options = {
allowAccountSelection: true,
autoApply: false,
...(requestOptions && typeof requestOptions === "object"
? requestOptions
: {}),
};
let promise;
try {
promise = this.createPendingModalTask("add-profile", { triggerElement });
} catch (error) {
this.setNextRequestLoginOptions(null);
throw error;
}
this.setNextRequestLoginOptions(() => ({ ...options }));
return promise.finally(() => {
this.setNextRequestLoginOptions(null);
});
}
getNostrClient() {
const direct = this.services?.nostrClient;
if (direct && typeof direct === "object") {
@@ -1742,14 +2062,25 @@ export default class LoginModalController {
result?.pubkey,
);
const consumed = this.resolvePendingTask(result, { type: "add-profile" });
const shouldClose =
result &&
typeof result === "object" &&
typeof result.pubkey === "string" &&
result.pubkey.trim();
if (shouldClose) {
this.helpers.closeModal();
}
if (consumed) {
return;
}
await safeInvokeAsync(this.callbacks.onLoginSuccess, {
provider: entry.provider.source || entry.provider,
result,
});
if (result && typeof result.pubkey === "string" && result.pubkey.trim()) {
this.helpers.closeModal();
}
} catch (error) {
devLogger.error(
`[LoginModalController] Login failed for ${providerId}:`,
@@ -1770,9 +2101,35 @@ export default class LoginModalController {
);
}
const rejectionError =
error instanceof Error
? error
: new Error(message || "Failed to login. Please try again.");
if (message && message !== rejectionError.message) {
rejectionError.message = message;
}
if (
error &&
typeof error === "object" &&
typeof error.code === "string" &&
!rejectionError.code
) {
rejectionError.code = error.code;
}
const consumed = this.rejectPendingTask(rejectionError, {
type: "add-profile",
});
if (consumed) {
return;
}
await safeInvokeAsync(this.callbacks.onLoginError, {
provider: entry.provider.source || entry.provider,
error,
error: rejectionError,
message,
});
} finally {
@@ -1836,6 +2193,19 @@ export default class LoginModalController {
}
destroy() {
if (this.pendingTask && typeof this.pendingTask.reject === "function") {
try {
this.pendingTask.reject(this.createCancellationError());
} catch (error) {
devLogger.warn(
"[LoginModalController] Failed to cancel pending login task during destroy:",
error,
);
}
}
this.pendingTask = null;
if (this.providerContainer && this.boundClickHandler) {
this.providerContainer.removeEventListener("click", this.boundClickHandler);
}
+240 -1
View File
@@ -25,6 +25,12 @@ const SECRET_PLACEHOLDER = "*****";
const DEFAULT_MAX_WALLET_DEFAULT_ZAP = 100000000;
const DEFAULT_SAVED_PROFILE_LABEL = "Saved profile";
const ADD_PROFILE_CANCELLATION_CODES = new Set([
"login-cancelled",
"user-cancelled",
"modal-dismissed",
]);
const PROVIDER_BADGE_BASE_CLASS =
"text-3xs font-semibold uppercase tracking-extra-wide";
const PROVIDER_BADGE_VARIANT_CLASS_MAP = Object.freeze({
@@ -154,6 +160,25 @@ const SERVICE_CONTRACT = [
"Formats npub strings for display using the canonical short representation (npubXXXX…XXXX).",
fallback: () => (value) => (typeof value === "string" ? value : ""),
},
{
key: "requestAddProfileLogin",
type: "function",
description:
"Opens the login modal and resolves with the authentication result for the add-profile flow.",
fallback: () => async () => {
throw new Error("Login service unavailable.");
},
},
{
key: "describeLoginError",
type: "function",
description:
"Maps authentication errors to human-readable strings for add-profile messaging.",
fallback: () => (_, fallbackMessage) =>
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Failed to login. Please try again.",
},
{
key: "getProfileCacheEntry",
type: "function",
@@ -766,6 +791,14 @@ export class ProfileModalController {
this.describeAdminErrorService = this.services.describeAdminError;
this.describeNotificationErrorService =
this.services.describeNotificationError;
this.describeLoginErrorService = this.services.describeLoginError;
this.requestAddProfileLoginService = this.services.requestAddProfileLogin;
this.log =
typeof this.services.log === "function"
? this.services.log
: (...args) => {
devLogger.log(...args);
};
this.callbacks = {
onClose: callbacks.onClose || noop,
@@ -920,6 +953,7 @@ export class ProfileModalController {
this.lastMobileViewState = "menu";
this.setActivePane(this.getActivePane());
this.setWalletPaneBusy(this.isWalletBusy());
this.addAccountButtonState = null;
this.adminEmptyMessages = new Map();
}
@@ -1223,7 +1257,7 @@ export class ProfileModalController {
if (this.addAccountButton instanceof HTMLElement) {
this.addAccountButton.addEventListener("click", () => {
this.callbacks.onAddAccount(this);
void this.handleAddAccountRequest();
});
}
@@ -1382,6 +1416,211 @@ export class ProfileModalController {
}
}
resolveAddAccountLoginError(error, fallbackMessage = "") {
const describe = this.describeLoginErrorService;
const fallback =
typeof fallbackMessage === "string" && fallbackMessage.trim()
? fallbackMessage.trim()
: "Couldn't add that profile. Please try again.";
if (typeof describe === "function") {
try {
const message = describe(error, fallback);
if (typeof message === "string" && message.trim()) {
return message.trim();
}
} catch (describeError) {
devLogger.warn(
"[ProfileModalController] describeLoginError service threw:",
describeError,
);
}
}
return fallback;
}
setAddAccountLoading(isLoading) {
if (!(this.addAccountButton instanceof HTMLElement)) {
return;
}
const button = this.addAccountButton;
const titleEl = button.querySelector("[data-profile-add-title]");
const hintEl = button.querySelector("[data-profile-add-hint]");
if (isLoading) {
this.addAccountButtonState = {
originalDisabled: button.disabled,
originalAriaLabel: button.getAttribute("aria-label"),
titleElement: titleEl instanceof HTMLElement ? titleEl : null,
hintElement: hintEl instanceof HTMLElement ? hintEl : null,
originalTitle:
titleEl instanceof HTMLElement ? titleEl.textContent || "" : "",
originalHint:
hintEl instanceof HTMLElement ? hintEl.textContent || "" : "",
};
button.disabled = true;
button.dataset.state = "loading";
button.setAttribute("aria-busy", "true");
button.setAttribute("aria-disabled", "true");
if (this.addAccountButtonState.titleElement) {
this.addAccountButtonState.titleElement.textContent = "Connecting...";
}
if (this.addAccountButtonState.hintElement) {
this.addAccountButtonState.hintElement.textContent =
"Complete the login prompt from your provider.";
}
button.setAttribute(
"aria-label",
"Connecting to your Nostr account",
);
return;
}
const state = this.addAccountButtonState;
if (state) {
button.disabled = !!state.originalDisabled;
if (state.originalDisabled) {
button.setAttribute("aria-disabled", "true");
} else {
button.removeAttribute("aria-disabled");
}
if (state.titleElement) {
state.titleElement.textContent = state.originalTitle || "";
}
if (state.hintElement) {
state.hintElement.textContent = state.originalHint || "";
}
if (state.originalAriaLabel === null) {
button.removeAttribute("aria-label");
} else if (typeof state.originalAriaLabel === "string") {
button.setAttribute("aria-label", state.originalAriaLabel);
}
} else {
if (button.disabled) {
button.setAttribute("aria-disabled", "true");
} else {
button.removeAttribute("aria-disabled");
}
}
button.setAttribute("aria-busy", "false");
delete button.dataset.state;
this.addAccountButtonState = null;
}
isAddAccountCancellationError(error) {
if (!error || typeof error !== "object") {
return false;
}
const code =
typeof error.code === "string" && error.code.trim()
? error.code.trim()
: "";
if (!code) {
return false;
}
return ADD_PROFILE_CANCELLATION_CODES.has(code);
}
async handleAddAccountRequest() {
if (!(this.addAccountButton instanceof HTMLElement)) {
return;
}
if (this.addAccountButton.dataset.state === "loading") {
return;
}
const requestLogin = this.requestAddProfileLoginService;
if (typeof requestLogin !== "function") {
devLogger.warn(
"[ProfileModalController] requestAddProfileLogin service unavailable.",
);
this.showError("Login is unavailable right now. Please try again later.");
return;
}
this.setAddAccountLoading(true);
try {
const loginResult = await requestLogin({
controller: this,
triggerElement: this.addAccountButton,
});
if (loginResult === undefined) {
return;
}
await this.invokeAddAccountCallback(loginResult);
} catch (error) {
if (this.isAddAccountCancellationError(error)) {
try {
this.log(
"[ProfileModalController] Add profile flow cancelled by user.",
error,
);
} catch (logError) {
devLogger.warn(
"[ProfileModalController] Failed to log cancellation event:",
logError,
);
}
return;
}
devLogger.error(
"[ProfileModalController] Failed to complete add profile authentication:",
error,
);
const message = this.resolveAddAccountLoginError(
error,
"Couldn't add that profile. Please try again.",
);
if (message) {
this.showError(message);
}
} finally {
this.setAddAccountLoading(false);
}
}
async invokeAddAccountCallback(loginResult) {
if (typeof this.callbacks.onAddAccount !== "function") {
return;
}
try {
await this.callbacks.onAddAccount({
controller: this,
loginResult,
});
} catch (error) {
devLogger.warn(
"[ProfileModalController] onAddAccount callback threw:",
error,
);
throw error;
}
}
renderSavedProfiles() {
const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey());
const entriesNeedingFetch = new Set();