From 0c211cd411c3c3ea1155efc8c185e835ec95d780 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:17:16 -0400 Subject: [PATCH 1/3] Fix zap dialog behavior for logged-out users --- js/app.js | 19 +++++++++---- js/ui/components/VideoModal.js | 4 +++ tests/video-modal-zap.test.mjs | 52 ++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/js/app.js b/js/app.js index 882cf31f..ad54ecbd 100644 --- a/js/app.js +++ b/js/app.js @@ -692,12 +692,21 @@ class Application { }; this.boundVideoModalZapOpenHandler = (event) => { const requiresLogin = Boolean(event?.detail?.requiresLogin); - if (requiresLogin) { - this.pendingModalZapOpen = true; - } else { - this.pendingModalZapOpen = false; + this.pendingModalZapOpen = requiresLogin; + + const openResult = this.zapController?.open({ requiresLogin }); + if (!openResult) { + if (!requiresLogin) { + this.pendingModalZapOpen = false; + } + this.videoModal?.closeZapDialog?.({ + silent: true, + restoreFocus: false, + }); + return; } - this.zapController?.open({ requiresLogin }); + + this.pendingModalZapOpen = false; }; this.boundVideoModalZapCloseHandler = () => { this.zapController?.close(); diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index cb3cc0b2..9ced7731 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -1054,6 +1054,10 @@ export class VideoModal { } async openZapDialog() { + if (this.modalZapRequiresLogin) { + return Promise.resolve(false); + } + if (this.modalZapOpenPromise) { return this.modalZapOpenPromise; } diff --git a/tests/video-modal-zap.test.mjs b/tests/video-modal-zap.test.mjs index e97639b6..96e6bcd6 100644 --- a/tests/video-modal-zap.test.mjs +++ b/tests/video-modal-zap.test.mjs @@ -183,9 +183,16 @@ function createVideoModalStub() { setCopyEnabled: noop, setShareEnabled: noop, setZapVisibility: noop, + dialogOpen: false, + closeZapDialogCalls: [], + closeZapDialog(options = {}) { + this.closeZapDialogCalls.push(options); + this.dialogOpen = false; + }, resetStats: noop, updateViewCountLabel: noop, setViewCountPointer: noop, + completedStates: [], addEventListener(type, handler) { listeners.set(type, handler); }, @@ -202,6 +209,9 @@ function createVideoModalStub() { setZapPending(value) { this.setZapPendingCalls.push(Boolean(value)); }, + setZapCompleted(value) { + this.completedStates.push(Boolean(value)); + }, setZapStatus(message, tone) { this.statusMessages.push({ message, tone }); }, @@ -297,6 +307,48 @@ async function createApp({ splitAndZap }) { return { app, modalStub, creatorAddress, platformAddress }; } +await (async () => { + // Test: logged-out zap request closes modal and queues reopen + const { app, modalStub } = await createApp({ + splitAndZap: async () => ({ receipts: [] }), + }); + + let loginNotificationCount = 0; + app.zapController.notifyLoginRequired = () => { + loginNotificationCount += 1; + }; + + modalStub.dialogOpen = true; + modalStub.closeZapDialogCalls = []; + + app.pendingModalZapOpen = false; + + app.boundVideoModalZapOpenHandler({ detail: { requiresLogin: true } }); + + assert.equal( + loginNotificationCount, + 1, + "should request login notification when zapping logged out", + ); + assert.equal( + app.pendingModalZapOpen, + true, + "zap should remain pending for post-login reopen", + ); + assert.equal( + modalStub.closeZapDialogCalls.length, + 1, + "zap dialog should close immediately when login required", + ); + assert.deepEqual(modalStub.closeZapDialogCalls[0], { + silent: true, + restoreFocus: false, + }); + assert.equal(modalStub.dialogOpen, false, "zap dialog should be closed"); + + app.destroy(); +})(); + await (async () => { // Test: wallet required before zapping const splitCalls = []; From ed1166c16e5143431ff1c4c14c6b573f44a6ef61 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:28:47 -0400 Subject: [PATCH 2/3] Treat session actors as logged out for zaps --- js/app.js | 14 ++++++++++- tests/video-modal-zap.test.mjs | 45 +++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/js/app.js b/js/app.js index ad54ecbd..df2a9db0 100644 --- a/js/app.js +++ b/js/app.js @@ -3038,7 +3038,19 @@ class Application { } isUserLoggedIn() { - return Boolean(this.normalizeHexPubkey(this.pubkey)); + const normalizedPubkey = this.normalizeHexPubkey(this.pubkey); + if (!normalizedPubkey) { + return false; + } + + const sessionActorPubkey = this.normalizeHexPubkey( + nostrClient?.sessionActor?.pubkey, + ); + if (sessionActorPubkey && sessionActorPubkey === normalizedPubkey) { + return false; + } + + return true; } async updateActiveNwcSettings(partial = {}) { diff --git a/tests/video-modal-zap.test.mjs b/tests/video-modal-zap.test.mjs index 96e6bcd6..e259bf5c 100644 --- a/tests/video-modal-zap.test.mjs +++ b/tests/video-modal-zap.test.mjs @@ -182,7 +182,16 @@ function createVideoModalStub() { return { setCopyEnabled: noop, setShareEnabled: noop, - setZapVisibility: noop, + setWalletPromptVisible: noop, + zapVisibilityCalls: [], + setZapVisibility(config) { + const payload = + config && typeof config === "object" + ? { ...config } + : { visible: Boolean(config) }; + this.zapVisibilityCalls.push(payload); + this.lastZapVisibility = payload; + }, dialogOpen: false, closeZapDialogCalls: [], closeZapDialog(options = {}) { @@ -349,6 +358,40 @@ await (async () => { app.destroy(); })(); +await (async () => { + // Test: session actors should not be treated as logged-in users for zaps + const { app, modalStub } = await createApp({ + splitAndZap: async () => ({ receipts: [] }), + }); + + const { nostrClient } = await import("../js/nostr.js"); + const originalSessionActor = nostrClient.sessionActor; + const sessionPubkey = "a".repeat(64); + nostrClient.sessionActor = { pubkey: sessionPubkey, privateKey: "priv" }; + + app.pubkey = sessionPubkey; + + modalStub.zapVisibilityCalls = []; + app.zapController.setVisibility(true); + + const lastVisibility = + modalStub.zapVisibilityCalls[modalStub.zapVisibilityCalls.length - 1]; + assert.deepEqual( + lastVisibility, + { visible: true, requiresLogin: true }, + "zap controller should require login when only a session actor is present", + ); + assert.equal( + app.isUserLoggedIn(), + false, + "session actor pubkeys should not count as logged-in users", + ); + + app.pubkey = null; + nostrClient.sessionActor = originalSessionActor; + app.destroy(); +})(); + await (async () => { // Test: wallet required before zapping const splitCalls = []; From 24cedbd5ac261ac050239c00423323da8083d2c8 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:43:42 -0400 Subject: [PATCH 3/3] Prevent zap popover for logged-out viewers --- js/app.js | 1 + js/ui/components/VideoModal.js | 33 ++++++++++++++++------ tests/video-modal-zap.test.mjs | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/js/app.js b/js/app.js index df2a9db0..16195666 100644 --- a/js/app.js +++ b/js/app.js @@ -696,6 +696,7 @@ class Application { const openResult = this.zapController?.open({ requiresLogin }); if (!openResult) { + event?.preventDefault?.(); if (!requiresLogin) { this.pendingModalZapOpen = false; } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 9ced7731..938ed1eb 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -128,9 +128,14 @@ export class VideoModal { this.eventTarget.removeEventListener(type, listener, options); } - dispatch(type, detail) { - const event = new CustomEvent(type, { detail }); - this.eventTarget.dispatchEvent(event); + dispatch(type, detail, options = {}) { + const config = + options && typeof options === "object" ? { ...options } : Object.create(null); + const event = new CustomEvent(type, { + detail, + cancelable: Boolean(config.cancelable), + }); + return this.eventTarget.dispatchEvent(event); } getRoot() { @@ -420,10 +425,14 @@ export class VideoModal { } if (this.modalZapRequiresLogin) { - this.dispatch("zap:open", { - video: this.activeVideo, - requiresLogin: true, - }); + this.dispatch( + "zap:open", + { + video: this.activeVideo, + requiresLogin: true, + }, + { cancelable: false }, + ); return; } @@ -446,9 +455,17 @@ export class VideoModal { return; } + const allowed = this.dispatch( + "zap:open", + { video: this.activeVideo }, + { cancelable: true }, + ); + if (allowed === false) { + return; + } + this.modalZapPendingToggle = null; this.openZapDialog(); - this.dispatch("zap:open", { video: this.activeVideo }); }); } diff --git a/tests/video-modal-zap.test.mjs b/tests/video-modal-zap.test.mjs index e259bf5c..bd21c978 100644 --- a/tests/video-modal-zap.test.mjs +++ b/tests/video-modal-zap.test.mjs @@ -358,6 +358,56 @@ await (async () => { app.destroy(); })(); +await (async () => { + // Test: preventDefault is called when zap opening is rejected post-dispatch + const { app, modalStub } = await createApp({ + splitAndZap: async () => ({ receipts: [] }), + }); + + let loginNotificationCount = 0; + app.zapController.notifyLoginRequired = () => { + loginNotificationCount += 1; + }; + + app.zapController.isUserLoggedIn = () => false; + + modalStub.closeZapDialogCalls = []; + + const preventCalls = []; + const event = { + detail: { requiresLogin: false }, + preventDefault: () => { + preventCalls.push(true); + }, + }; + + app.pendingModalZapOpen = true; + + app.boundVideoModalZapOpenHandler(event); + + assert.equal( + loginNotificationCount, + 1, + "should notify login requirement when zap open is rejected", + ); + assert.equal( + preventCalls.length, + 1, + "should prevent default zap opening when controller rejects it", + ); + assert.equal( + app.pendingModalZapOpen, + false, + "pending zap reopen flag should clear when rejection wasn't login-gated", + ); + assert.deepEqual(modalStub.closeZapDialogCalls[0], { + silent: true, + restoreFocus: false, + }); + + app.destroy(); +})(); + await (async () => { // Test: session actors should not be treated as logged-in users for zaps const { app, modalStub } = await createApp({