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({