Fix zap login notification handling

This commit is contained in:
thePR0M3TH3AN committed 2025-10-18 20:04:58 -04:00
1 parent 841f1dd7f5
commit 72033a900b
4 files changed
+262 -166

No files matched your search

+30 -3
View File
@@ -57,7 +57,7 @@ import {
ingestLocalViewEvent,
} from "./viewCounter.js";
import { splitAndZap as splitAndZapDefault } from "./payments/zapSplit.js";
import { LOGIN_TO_ZAP_MESSAGE } from "./payments/zapMessages.js";
import { showLoginRequiredToZapNotification } from "./payments/zapNotifications.js";
import {
formatAbsoluteTimestamp as formatAbsoluteTimestampUtil,
formatTimeAgo as formatTimeAgoUtil,
@@ -209,6 +209,7 @@ class Application {
this.boundVideoModalCreatorHandler = null;
this.boundVideoModalZapHandler = null;
this.boundVideoModalZapWalletHandler = null;
this.pendingModalZapOpen = false;
this.videoListViewPlaybackHandler = null;
this.videoListViewEditHandler = null;
this.videoListViewRevertHandler = null;
@@ -591,6 +592,8 @@ class Application {
typeof nostrClient?.sessionActor?.pubkey === "string" &&
nostrClient.sessionActor.pubkey.trim()
),
notifyLoginRequired: () =>
showLoginRequiredToZapNotification({ app: this }),
splitAndZap: (...args) => this.splitAndZap(...args),
payments: this.payments,
callbacks: {
@@ -651,9 +654,11 @@ class Application {
this.boundVideoModalZapOpenHandler = (event) => {
const requiresLogin = Boolean(event?.detail?.requiresLogin);
if (requiresLogin) {
this.showStatus(LOGIN_TO_ZAP_MESSAGE, { autoHideMs: 5000 });
this.pendingModalZapOpen = true;
} else {
this.pendingModalZapOpen = false;
}
this.zapController?.open();
this.zapController?.open({ requiresLogin });
};
this.boundVideoModalZapCloseHandler = () => {
this.zapController?.close();
@@ -3158,6 +3163,27 @@ class Application {
}
}
const shouldReopenZap = this.pendingModalZapOpen;
this.pendingModalZapOpen = false;
if (shouldReopenZap) {
const hasLightning = Boolean(this.currentVideo?.lightningAddress);
if (hasLightning && this.videoModal?.openZapDialog) {
Promise.resolve()
.then(() => this.videoModal.openZapDialog())
.then((opened) => {
if (opened) {
this.zapController?.open();
}
})
.catch((error) => {
devLogger.warn(
"[Application] Failed to reopen zap dialog after login:",
error,
);
});
}
}
this.dispatchAuthChange({
status: "login",
loggedIn: true,
@@ -3235,6 +3261,7 @@ class Application {
async handleAuthLogout(detail = {}) {
this.resetViewLoggingState();
this.pendingModalZapOpen = false;
await this.nwcSettingsService.onLogout({
pubkey: detail?.pubkey || this.pubkey,
+20 -160
View File
@@ -33,7 +33,7 @@ import {
validateInvoiceAmount
} from "./payments/zapSharedState.js";
import { splitAndZap } from "./payments/zapSplit.js";
import { LOGIN_TO_ZAP_MESSAGE } from "./payments/zapMessages.js";
import { showLoginRequiredToZapNotification } from "./payments/zapNotifications.js";
import {
resolveLightningAddress,
fetchPayServiceData,
@@ -48,168 +48,10 @@ import {
const getApp = () => getApplication();
const LOGIN_NOTIFICATION_AUTO_HIDE_MS = 5000;
function syncNotificationPortalVisibility(doc) {
if (!doc) {
return;
}
const HTMLElementCtor =
doc.defaultView?.HTMLElement ||
(typeof HTMLElement !== "undefined" ? HTMLElement : null);
if (!HTMLElementCtor) {
return;
}
const portal = doc.getElementById("notificationPortal");
if (!portal || !(portal instanceof HTMLElementCtor)) {
return;
}
const banners = portal.querySelectorAll(".notification-banner");
const hasVisibleBanner = Array.from(banners).some(
(banner) =>
banner instanceof HTMLElementCtor && !banner.classList.contains("hidden"),
);
portal.classList.toggle("notification-portal--active", hasVisibleBanner);
}
function showLoginRequiredToZapNotification() {
const app = getApp();
if (app) {
if (typeof app.showStatus === "function") {
app.showStatus(LOGIN_TO_ZAP_MESSAGE, {
autoHideMs: LOGIN_NOTIFICATION_AUTO_HIDE_MS,
});
return;
}
if (typeof app.showError === "function") {
app.showError(LOGIN_TO_ZAP_MESSAGE);
return;
}
}
const doc = typeof document !== "undefined" ? document : null;
if (!doc) {
return;
}
const HTMLElementCtor =
doc.defaultView?.HTMLElement ||
(typeof HTMLElement !== "undefined" ? HTMLElement : null);
if (!HTMLElementCtor) {
return;
}
const statusContainer = doc.getElementById("statusContainer");
if (statusContainer && statusContainer instanceof HTMLElementCtor) {
const messageTarget =
statusContainer.querySelector("[data-status-message]") || null;
if (messageTarget && messageTarget instanceof HTMLElementCtor) {
messageTarget.textContent = LOGIN_TO_ZAP_MESSAGE;
} else {
statusContainer.textContent = LOGIN_TO_ZAP_MESSAGE;
}
statusContainer.classList.remove("hidden");
syncNotificationPortalVisibility(doc);
const schedule =
doc.defaultView?.setTimeout ||
(typeof setTimeout === "function" ? setTimeout : null);
if (typeof schedule === "function") {
const initialTarget =
messageTarget && messageTarget instanceof HTMLElementCtor
? messageTarget
: statusContainer;
const expectedText = initialTarget.textContent;
schedule(() => {
if (!doc || typeof doc.contains !== "function") {
return;
}
if (!doc.contains(statusContainer)) {
return;
}
const activeTarget =
statusContainer.querySelector("[data-status-message]") || null;
const resolvedTarget =
activeTarget && activeTarget instanceof HTMLElementCtor
? activeTarget
: messageTarget && messageTarget instanceof HTMLElementCtor
? messageTarget
: statusContainer;
if (resolvedTarget.textContent !== expectedText) {
return;
}
if (activeTarget && activeTarget instanceof HTMLElementCtor) {
activeTarget.textContent = "";
} else if (messageTarget && messageTarget instanceof HTMLElementCtor) {
messageTarget.textContent = "";
} else {
statusContainer.textContent = "";
}
statusContainer.classList.add("hidden");
syncNotificationPortalVisibility(doc);
}, LOGIN_NOTIFICATION_AUTO_HIDE_MS);
}
return;
}
const errorContainer = doc.getElementById("errorContainer");
if (!errorContainer || !(errorContainer instanceof HTMLElementCtor)) {
return;
}
errorContainer.textContent = LOGIN_TO_ZAP_MESSAGE;
errorContainer.classList.remove("hidden");
syncNotificationPortalVisibility(doc);
const schedule =
doc.defaultView?.setTimeout ||
(typeof setTimeout === "function" ? setTimeout : null);
if (typeof schedule !== "function") {
return;
}
schedule(() => {
if (!doc || typeof doc.contains !== "function") {
return;
}
if (!doc.contains(errorContainer)) {
return;
}
if (errorContainer.textContent !== LOGIN_TO_ZAP_MESSAGE) {
return;
}
errorContainer.textContent = "";
errorContainer.classList.add("hidden");
syncNotificationPortalVisibility(doc);
}, LOGIN_NOTIFICATION_AUTO_HIDE_MS);
}
let currentChannelHex = null;
let currentChannelNpub = null;
let currentChannelLightningAddress = "";
let channelZapPendingOpen = false;
const summarizeZapTracker = (tracker) =>
Array.isArray(tracker)
@@ -1913,10 +1755,13 @@ function handleZapButtonClick(event) {
const requiresLogin = zapButton.dataset?.requiresLogin === "true";
if (requiresLogin || isSessionActorWithoutLogin()) {
channelZapPendingOpen = true;
showLoginRequiredToZapNotification();
return;
}
channelZapPendingOpen = false;
if (!zapPopover || zapPopoverTrigger !== zapButton) {
setupZapButton({ force: true });
}
@@ -4023,6 +3868,7 @@ window.addEventListener("bitvid:auth-changed", (event) => {
resetZapRetryState();
setZapStatus("", "neutral");
clearZapReceipts();
channelZapPendingOpen = false;
const amountInput = getZapAmountInput();
if (amountInput) {
amountInput.value = "";
@@ -4034,6 +3880,20 @@ window.addEventListener("bitvid:auth-changed", (event) => {
zapInFlight = false;
resetZapRetryState();
setZapStatus("", "neutral");
if (channelZapPendingOpen) {
channelZapPendingOpen = false;
const zapButton = getChannelZapButton();
const requiresLogin =
zapButton?.dataset?.requiresLogin === "true" ||
zapButton?.hasAttribute("hidden");
if (zapButton && !requiresLogin) {
const opened = openZapControls({ focus: true });
if (!opened) {
setupZapButton({ force: true });
openZapControls({ focus: true });
}
}
}
return;
}
});
+189
View File
@@ -0,0 +1,189 @@
// js/payments/zapNotifications.js
import { getApplication } from "../applicationContext.js";
import { LOGIN_TO_ZAP_MESSAGE } from "./zapMessages.js";
const LOGIN_NOTIFICATION_AUTO_HIDE_MS = 5000;
function resolveDocument(candidate) {
if (candidate && typeof candidate === "object") {
return candidate;
}
if (typeof document !== "undefined") {
return document;
}
return null;
}
function resolveHTMLElementCtor(doc) {
if (!doc) {
return null;
}
return (
doc.defaultView?.HTMLElement ||
(typeof HTMLElement !== "undefined" ? HTMLElement : null)
);
}
export function syncNotificationPortalVisibility(docCandidate) {
const doc = resolveDocument(docCandidate);
if (!doc) {
return;
}
const HTMLElementCtor = resolveHTMLElementCtor(doc);
if (!HTMLElementCtor) {
return;
}
const portal = doc.getElementById("notificationPortal");
if (!portal || !(portal instanceof HTMLElementCtor)) {
return;
}
const banners = portal.querySelectorAll(".notification-banner");
const hasVisibleBanner = Array.from(banners).some((banner) => {
return (
banner instanceof HTMLElementCtor && !banner.classList.contains("hidden")
);
});
portal.classList.toggle("notification-portal--active", hasVisibleBanner);
}
function scheduleAutoHide({
container,
messageTarget,
expectedText,
doc,
autoHideMs,
}) {
if (!Number.isFinite(autoHideMs) || autoHideMs <= 0) {
return;
}
const scheduler =
doc?.defaultView?.setTimeout ||
(typeof setTimeout === "function" ? setTimeout : null);
if (typeof scheduler !== "function") {
return;
}
scheduler(() => {
if (!doc || typeof doc.contains !== "function") {
return;
}
if (!doc.contains(container)) {
return;
}
const HTMLElementCtor = resolveHTMLElementCtor(doc);
if (!HTMLElementCtor) {
return;
}
const activeTarget =
container.querySelector("[data-status-message]") || messageTarget;
const resolvedTarget =
activeTarget && activeTarget instanceof HTMLElementCtor
? activeTarget
: messageTarget && messageTarget instanceof HTMLElementCtor
? messageTarget
: container;
if (resolvedTarget.textContent !== expectedText) {
return;
}
if (resolvedTarget !== container) {
resolvedTarget.textContent = "";
} else {
container.textContent = "";
}
container.classList.add("hidden");
syncNotificationPortalVisibility(doc);
}, autoHideMs);
}
export function showLoginRequiredToZapNotification({
app: appCandidate,
document: docCandidate,
autoHideMs = LOGIN_NOTIFICATION_AUTO_HIDE_MS,
} = {}) {
const app = appCandidate || getApplication();
if (app && typeof app.showStatus === "function") {
app.showStatus(LOGIN_TO_ZAP_MESSAGE, { autoHideMs });
return true;
}
if (app && typeof app.showError === "function") {
app.showError(LOGIN_TO_ZAP_MESSAGE);
return true;
}
const doc = resolveDocument(docCandidate);
if (!doc) {
return false;
}
const HTMLElementCtor = resolveHTMLElementCtor(doc);
if (!HTMLElementCtor) {
return false;
}
const statusContainer = doc.getElementById("statusContainer");
if (statusContainer instanceof HTMLElementCtor) {
const messageTarget =
statusContainer.querySelector("[data-status-message]") || null;
if (messageTarget && messageTarget instanceof HTMLElementCtor) {
messageTarget.textContent = LOGIN_TO_ZAP_MESSAGE;
} else {
statusContainer.textContent = LOGIN_TO_ZAP_MESSAGE;
}
statusContainer.classList.remove("hidden");
syncNotificationPortalVisibility(doc);
const expectedText =
messageTarget && messageTarget instanceof HTMLElementCtor
? messageTarget.textContent
: statusContainer.textContent;
scheduleAutoHide({
container: statusContainer,
messageTarget,
expectedText,
doc,
autoHideMs,
});
return true;
}
const errorContainer = doc.getElementById("errorContainer");
if (errorContainer instanceof HTMLElementCtor) {
errorContainer.textContent = LOGIN_TO_ZAP_MESSAGE;
errorContainer.classList.remove("hidden");
syncNotificationPortalVisibility(doc);
scheduleAutoHide({
container: errorContainer,
messageTarget: null,
expectedText: LOGIN_TO_ZAP_MESSAGE,
doc,
autoHideMs,
});
return true;
}
return false;
}
+23 -3
View File
@@ -1,4 +1,5 @@
import { isDevMode } from "../config.js";
import { LOGIN_TO_ZAP_MESSAGE } from "../payments/zapMessages.js";
import {
calculateZapShares,
fetchLightningMetadata,
@@ -36,6 +37,7 @@ export default class ZapController {
hasSessionActor,
hasActiveWalletConnection,
splitAndZap,
notifyLoginRequired,
payments = {},
callbacks = {},
requestWalletPane,
@@ -74,6 +76,8 @@ export default class ZapController {
}
this.splitAndZap =
typeof splitAndZap === "function" ? splitAndZap : () => Promise.resolve();
this.notifyLoginRequired =
typeof notifyLoginRequired === "function" ? notifyLoginRequired : null;
this.payments = payments || {};
this.callbacks = {
onSuccess: typeof callbacks.onSuccess === "function" ? callbacks.onSuccess : null,
@@ -127,16 +131,32 @@ export default class ZapController {
}
}
open() {
open(options = {}) {
const config =
options && typeof options === "object" ? options : Object.create(null);
const requiresLogin = Boolean(config.requiresLogin);
this.resetFeedback();
if (requiresLogin) {
if (this.notifyLoginRequired) {
this.notifyLoginRequired();
} else {
this.notifyError(LOGIN_TO_ZAP_MESSAGE);
}
return false;
}
if (!this.isUserLoggedIn()) {
this.notifyError("Log in with your Nostr profile to send zaps.");
return;
if (this.notifyLoginRequired) {
this.notifyLoginRequired();
} else {
this.notifyError(LOGIN_TO_ZAP_MESSAGE);
}
return false;
}
this.applyDefaultAmount();
this.preloadLightningMetadata().catch((error) => {
devLogger.warn("[zap] Preload metadata error:", error);
});
return true;
}
close() {