mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #764 from PR0M3TH3AN/codex/review-nip-07-implementation-in-bitvid
Allow profile additions to use provider picker
This commit is contained in:
2 files changed
+385
-50
No files matched your search
@@ -412,6 +412,7 @@ class Application {
|
||||
// Profile and login modal controller state
|
||||
this.profileController = null;
|
||||
this.loginModalController = null;
|
||||
this.loginModalPendingTask = null;
|
||||
this.currentUserNpub = null;
|
||||
|
||||
try {
|
||||
@@ -436,49 +437,27 @@ class Application {
|
||||
`[LoginModal] Provider selected: ${providerId}.`,
|
||||
);
|
||||
},
|
||||
onLoginSuccess: async ({ result }) => {
|
||||
if (!result || typeof result !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { pubkey, detail } = result;
|
||||
devLogger.log(
|
||||
"[LoginModal] Login result returned pubkey:",
|
||||
pubkey,
|
||||
);
|
||||
|
||||
if (
|
||||
pubkey &&
|
||||
detail &&
|
||||
typeof detail === "object" &&
|
||||
detail.__handled !== true
|
||||
) {
|
||||
try {
|
||||
await this.handleAuthLogin(detail);
|
||||
} catch (error) {
|
||||
onLoginSuccess: (payload) => {
|
||||
const maybePromise = this.handleLoginModalSuccess(payload);
|
||||
if (maybePromise && typeof maybePromise.then === "function") {
|
||||
maybePromise.catch((error) => {
|
||||
devLogger.error(
|
||||
"[LoginModal] handleAuthLogin fallback failed:",
|
||||
"[LoginModal] handleLoginModalSuccess threw:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onLoginError: async ({ message, error, provider }) => {
|
||||
const fallbackMessage =
|
||||
typeof message === "string" && message.trim()
|
||||
? message.trim()
|
||||
: this.describeLoginError(
|
||||
error,
|
||||
provider?.errorMessage ||
|
||||
"Failed to login. Please try again.",
|
||||
);
|
||||
|
||||
userLogger.warn(
|
||||
provider && provider.id
|
||||
? `[LoginModal] Login failed for provider ${provider.id}.`
|
||||
: "[LoginModal] Login failed for provider.",
|
||||
);
|
||||
this.showError(fallbackMessage);
|
||||
onLoginError: (payload) => {
|
||||
const maybePromise = this.handleLoginModalError(payload);
|
||||
if (maybePromise && typeof maybePromise.then === "function") {
|
||||
maybePromise.catch((error) => {
|
||||
devLogger.error(
|
||||
"[LoginModal] handleLoginModalError threw:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
helpers: {
|
||||
@@ -1990,6 +1969,267 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
async handleLoginModalSuccess(payload = {}) {
|
||||
const result =
|
||||
payload && typeof payload === "object" ? payload.result || null : null;
|
||||
const pubkey =
|
||||
(result &&
|
||||
typeof result === "object" &&
|
||||
typeof result.pubkey === "string"
|
||||
? result.pubkey
|
||||
: typeof result === "string"
|
||||
? result
|
||||
: null) || null;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const { detail } = result;
|
||||
|
||||
if (
|
||||
pubkey &&
|
||||
detail &&
|
||||
typeof detail === "object" &&
|
||||
detail.__handled !== true
|
||||
) {
|
||||
try {
|
||||
await this.handleAuthLogin(detail);
|
||||
} catch (error) {
|
||||
devLogger.error(
|
||||
"[LoginModal] handleAuthLogin fallback failed:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleLoginModalError(payload = {}) {
|
||||
const message =
|
||||
typeof payload?.message === "string" && payload.message.trim()
|
||||
? payload.message.trim()
|
||||
: null;
|
||||
const error = payload?.error || null;
|
||||
const provider = payload?.provider || null;
|
||||
|
||||
const fallbackMessage =
|
||||
message ||
|
||||
this.describeLoginError(
|
||||
error,
|
||||
provider?.errorMessage || "Failed to login. Please try again.",
|
||||
);
|
||||
|
||||
const normalizedMessage =
|
||||
typeof fallbackMessage === "string" && fallbackMessage.trim()
|
||||
? 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}.`
|
||||
: "[LoginModal] Login failed for provider.",
|
||||
);
|
||||
this.showError(normalizedMessage);
|
||||
}
|
||||
|
||||
async requestProfileAdditionLogin({ triggerElement } = {}) {
|
||||
if (
|
||||
this.loginModalController &&
|
||||
typeof this.loginModalController.setNextRequestLoginOptions ===
|
||||
"function"
|
||||
) {
|
||||
try {
|
||||
return await this.openLoginModalForProfileAddition({
|
||||
triggerElement,
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
(error.code === "login-cancelled" || error.code === "user-cancelled")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
!error ||
|
||||
(error.code !== "modal-unavailable" &&
|
||||
error.code !== "modal-open-failed")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
devLogger.warn(
|
||||
"[profileModal] Falling back to direct login for profile addition:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.authService.requestLogin({
|
||||
allowAccountSelection: true,
|
||||
autoApply: false,
|
||||
});
|
||||
}
|
||||
|
||||
async openLoginModalForProfileAddition({ triggerElement } = {}) {
|
||||
const controller = this.loginModalController;
|
||||
if (!controller) {
|
||||
const error = new Error("Login modal unavailable.");
|
||||
error.code = "modal-unavailable";
|
||||
throw error;
|
||||
}
|
||||
|
||||
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.",
|
||||
);
|
||||
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) ||
|
||||
@@ -2029,11 +2269,11 @@ class Application {
|
||||
titleEl.textContent = "Connecting...";
|
||||
}
|
||||
if (hintEl) {
|
||||
hintEl.textContent = "Check your extension";
|
||||
hintEl.textContent = "Complete the login prompt from your provider.";
|
||||
}
|
||||
button.setAttribute(
|
||||
"aria-label",
|
||||
"Connecting to your Nostr extension",
|
||||
"Connecting to your Nostr account",
|
||||
);
|
||||
} else {
|
||||
if (titleEl) {
|
||||
@@ -2053,10 +2293,31 @@ class Application {
|
||||
setLoadingState(true);
|
||||
|
||||
try {
|
||||
const loginResult = await this.authService.requestLogin({
|
||||
allowAccountSelection: true,
|
||||
autoApply: false,
|
||||
});
|
||||
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
|
||||
@@ -2103,7 +2364,7 @@ class Application {
|
||||
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalizedPubkey) {
|
||||
throw new Error(
|
||||
"Received an invalid public key from the Nostr extension.",
|
||||
"Received an invalid public key from the authentication provider.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2142,7 +2403,25 @@ class Application {
|
||||
|
||||
this.showSuccess("Profile added. Select it when you're ready to switch.");
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to add profile via NIP-07:", 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.",
|
||||
|
||||
@@ -247,6 +247,8 @@ export default class LoginModalController {
|
||||
this.activeNip46Form = null;
|
||||
this.remoteSignerUnsubscribe = null;
|
||||
this.lastRemoteSignerStatus = null;
|
||||
this.nextRequestLoginOptions = null;
|
||||
this.nextRequestLoginOptionsResolver = null;
|
||||
|
||||
this.initializeRemoteSignerStatus();
|
||||
this.initialized = false;
|
||||
@@ -1300,11 +1302,18 @@ export default class LoginModalController {
|
||||
`[LoginModalController] Starting login for provider ${providerId}.`,
|
||||
);
|
||||
|
||||
const requestOptions = { providerId, ...providerOptions };
|
||||
|
||||
const extraOptions = await this.resolveNextRequestLoginOptions({
|
||||
provider: entry.provider.source || entry.provider,
|
||||
providerId,
|
||||
});
|
||||
if (extraOptions && typeof extraOptions === "object") {
|
||||
Object.assign(requestOptions, extraOptions);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.services.authService.requestLogin({
|
||||
providerId,
|
||||
...providerOptions,
|
||||
});
|
||||
const result = await this.services.authService.requestLogin(requestOptions);
|
||||
|
||||
devLogger.log(
|
||||
`[LoginModalController] Login resolved for ${providerId} with pubkey:`,
|
||||
@@ -1349,6 +1358,51 @@ export default class LoginModalController {
|
||||
}
|
||||
}
|
||||
|
||||
setNextRequestLoginOptions(options) {
|
||||
if (typeof options === "function") {
|
||||
this.nextRequestLoginOptionsResolver = options;
|
||||
this.nextRequestLoginOptions = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (options && typeof options === "object") {
|
||||
this.nextRequestLoginOptions = { ...options };
|
||||
this.nextRequestLoginOptionsResolver = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.nextRequestLoginOptions = null;
|
||||
this.nextRequestLoginOptionsResolver = null;
|
||||
}
|
||||
|
||||
async resolveNextRequestLoginOptions(context = {}) {
|
||||
if (typeof this.nextRequestLoginOptionsResolver === "function") {
|
||||
const resolver = this.nextRequestLoginOptionsResolver;
|
||||
this.nextRequestLoginOptionsResolver = null;
|
||||
this.nextRequestLoginOptions = null;
|
||||
try {
|
||||
const resolved = await resolver({ controller: this, ...context });
|
||||
if (resolved && typeof resolved === "object") {
|
||||
return { ...resolved };
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[LoginModalController] Next request options resolver threw:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this.nextRequestLoginOptions && typeof this.nextRequestLoginOptions === "object") {
|
||||
const options = this.nextRequestLoginOptions;
|
||||
this.nextRequestLoginOptions = null;
|
||||
return { ...options };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.providerContainer && this.boundClickHandler) {
|
||||
this.providerContainer.removeEventListener("click", this.boundClickHandler);
|
||||
@@ -1371,6 +1425,8 @@ export default class LoginModalController {
|
||||
this.remoteSignerUnsubscribe = null;
|
||||
|
||||
this.providerEntries.clear();
|
||||
this.nextRequestLoginOptions = null;
|
||||
this.nextRequestLoginOptionsResolver = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user