// js/payments/zapNotifications.js import { getApplication } from "../applicationContext.js"; import { LOGIN_TO_ZAP_MESSAGE } from "./zapMessages.js"; const LOGIN_NOTIFICATION_AUTO_HIDE_MS = 3000; function getOwnerDocument(node) { return node?.ownerDocument || null; } function isNotificationBannerActive({ container, messageTarget, expectedText, }) { const doc = getOwnerDocument(container); const HTMLElementCtor = resolveHTMLElementCtor(doc); if (!HTMLElementCtor || !(container instanceof HTMLElementCtor)) { return false; } if (container.classList.contains("hidden")) { return false; } const resolvedTarget = messageTarget && messageTarget instanceof HTMLElementCtor ? messageTarget : container; const text = typeof resolvedTarget?.textContent === "string" ? resolvedTarget.textContent.trim() : ""; return text === expectedText; } function attemptStatusBanner(app, { autoHideMs, expectedText, showSpinner }) { if (!app || typeof app.showStatus !== "function") { return false; } app.showStatus(expectedText, { autoHideMs, showSpinner }); return isNotificationBannerActive({ container: app.statusContainer, messageTarget: app.statusMessage, expectedText, }); } function attemptErrorBanner(app, { expectedText }) { if (!app || typeof app.showError !== "function") { return false; } app.showError(expectedText); return isNotificationBannerActive({ container: app.errorContainer, messageTarget: null, expectedText, }); } 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) ); } function ensureNotificationPortal({ doc, HTMLElementCtor }) { if (!doc || !HTMLElementCtor) { return null; } let portal = doc.getElementById("notificationPortal"); if (!(portal instanceof HTMLElementCtor)) { portal = doc.createElement("div"); portal.id = "notificationPortal"; portal.className = "notification-portal"; portal.setAttribute("role", "region"); portal.setAttribute("aria-live", "polite"); portal.setAttribute("aria-label", "System notifications"); const targetParent = doc.body || doc.documentElement || doc; targetParent.appendChild(portal); } return portal instanceof HTMLElementCtor ? portal : null; } function ensureStatusContainer({ doc, portal, HTMLElementCtor, includeSpinner = true, }) { if (!doc || !HTMLElementCtor) { return { container: null, messageTarget: null }; } let statusContainer = doc.getElementById("statusContainer"); if (!(statusContainer instanceof HTMLElementCtor)) { statusContainer = doc.createElement("div"); statusContainer.id = "statusContainer"; statusContainer.className = "notification-banner notification-banner--info status-banner hidden"; statusContainer.setAttribute("role", "status"); statusContainer.setAttribute("aria-live", "polite"); if (includeSpinner) { const spinner = doc.createElement("span"); spinner.className = "status-spinner"; spinner.setAttribute("aria-hidden", "true"); statusContainer.appendChild(spinner); } const message = doc.createElement("span"); message.className = "status-message"; message.dataset.statusMessage = ""; statusContainer.appendChild(message); } else { const existingSpinner = statusContainer.querySelector(".status-spinner"); if (includeSpinner) { if (!(existingSpinner instanceof HTMLElementCtor)) { const spinner = doc.createElement("span"); spinner.className = "status-spinner"; spinner.setAttribute("aria-hidden", "true"); statusContainer.insertBefore(spinner, statusContainer.firstChild); } } else if (existingSpinner instanceof HTMLElementCtor) { existingSpinner.remove(); } } let messageTarget = statusContainer.querySelector("[data-status-message]"); if (!(messageTarget instanceof HTMLElementCtor)) { messageTarget = doc.createElement("span"); messageTarget.className = "status-message"; messageTarget.dataset.statusMessage = ""; statusContainer.appendChild(messageTarget); } if (portal instanceof HTMLElementCtor && statusContainer.parentNode !== portal) { portal.appendChild(statusContainer); } return { container: statusContainer, messageTarget }; } function ensureErrorContainer({ doc, portal, HTMLElementCtor }) { if (!doc || !HTMLElementCtor) { return null; } let errorContainer = doc.getElementById("errorContainer"); if (!(errorContainer instanceof HTMLElementCtor)) { errorContainer = doc.createElement("div"); errorContainer.id = "errorContainer"; errorContainer.className = "notification-banner notification-banner--critical hidden"; } if (portal instanceof HTMLElementCtor && errorContainer.parentNode !== portal) { portal.appendChild(errorContainer); } return errorContainer instanceof HTMLElementCtor ? errorContainer : 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 expectedText = LOGIN_TO_ZAP_MESSAGE; const app = appCandidate || getApplication(); const resolvedDocCandidate = docCandidate || getOwnerDocument(app?.statusContainer) || getOwnerDocument(app?.errorContainer); if (attemptStatusBanner(app, { autoHideMs, expectedText, showSpinner: false })) { return true; } if (attemptErrorBanner(app, { expectedText })) { return true; } const doc = resolveDocument(resolvedDocCandidate); if (!doc) { return false; } const HTMLElementCtor = resolveHTMLElementCtor(doc); if (!HTMLElementCtor) { return false; } const portal = ensureNotificationPortal({ doc, HTMLElementCtor }); const { container: statusContainer, messageTarget } = ensureStatusContainer({ doc, portal, HTMLElementCtor, includeSpinner: false, }); if (statusContainer instanceof HTMLElementCtor) { if (messageTarget && messageTarget instanceof HTMLElementCtor) { messageTarget.textContent = expectedText; } else { statusContainer.textContent = expectedText; } statusContainer.classList.remove("hidden"); syncNotificationPortalVisibility(doc); const resolvedText = messageTarget && messageTarget instanceof HTMLElementCtor ? messageTarget.textContent : statusContainer.textContent; scheduleAutoHide({ container: statusContainer, messageTarget, expectedText: resolvedText, doc, autoHideMs, }); return true; } const errorContainer = ensureErrorContainer({ doc, portal, HTMLElementCtor, }); if (errorContainer instanceof HTMLElementCtor) { errorContainer.textContent = expectedText; errorContainer.classList.remove("hidden"); syncNotificationPortalVisibility(doc); scheduleAutoHide({ container: errorContainer, messageTarget: null, expectedText, doc, autoHideMs, }); return true; } return false; }