From 99c1a28e7673326c7ba74d9dff7da23a78afea15 Mon Sep 17 00:00:00 2001 From: Lukas Boehler Date: Thu, 22 Oct 2026 19:02:41 +0200 Subject: [PATCH] fix: honor disabled notifications for cached cards --- index.d.ts | 4 + src/Gleap.js | 5 + src/GleapNotificationManager.js | 10 +- src/GleapNotificationManager.test.js | 5 +- src/GleapNotifications.disabled.test.js | 125 ++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/GleapNotifications.disabled.test.js diff --git a/index.d.ts b/index.d.ts index 71d91ef4..eccfb39e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -170,6 +170,10 @@ export namespace Gleap { function attachNetworkLogs(networkLogs: string): void; function clearIdentity(): void; function setTags(tags: string[]): void; + /** + * Hide notification cards, including cached or already-visible cards, without + * hiding the support button or messenger. Reenable to show eligible cards again. + */ function setDisableInAppNotifications( disableInAppNotifications: boolean ): void; diff --git a/src/Gleap.js b/src/Gleap.js index 83908a06..a4f2d3a5 100755 --- a/src/Gleap.js +++ b/src/Gleap.js @@ -178,7 +178,12 @@ class Gleap { */ static setDisableInAppNotifications(disableInAppNotifications) { const instance = this.getInstance(); + if (instance.disableInAppNotifications === disableInAppNotifications) { + return; + } instance.disableInAppNotifications = disableInAppNotifications; + // Apply route changes to cards already on screen, without marking them read. + GleapNotificationManager.getInstance().renderNotifications(); } /** diff --git a/src/GleapNotificationManager.js b/src/GleapNotificationManager.js index 4ec9d82b..fdfa746e 100644 --- a/src/GleapNotificationManager.js +++ b/src/GleapNotificationManager.js @@ -135,6 +135,10 @@ export default class GleapNotificationManager { * rendered on an allowed page followed users onto excluded ones. */ getVisibleNotifications() { + // Cached cards and direct deliveries bypass performActions' arrival guard. + if (Gleap.getInstance().disableInAppNotifications) { + return []; + } return this.notifications.filter((notification) => this.notificationPassesPageRules(notification)); } @@ -199,7 +203,11 @@ export default class GleapNotificationManager { // Play sound only if it's a new one and its page rules allow the // current page (it may have arrived on an excluded page and only // become visible later). - if (notification.sound && this.notificationPassesPageRules(notification)) { + if ( + notification.sound && + !Gleap.getInstance().disableInAppNotifications && + this.notificationPassesPageRules(notification) + ) { GleapAudioManager.ping(); } } diff --git a/src/GleapNotificationManager.test.js b/src/GleapNotificationManager.test.js index 37afd7f4..d55aca13 100644 --- a/src/GleapNotificationManager.test.js +++ b/src/GleapNotificationManager.test.js @@ -1,4 +1,4 @@ -import { GleapTabCommunication, GleapSession, GleapAudioManager } from './Gleap'; +import Gleap, { GleapTabCommunication, GleapSession, GleapAudioManager } from './Gleap'; import { loadFromGleapCache, saveToGleapCache } from './GleapHelper'; import GleapNotificationManager from './GleapNotificationManager'; @@ -7,7 +7,7 @@ import GleapNotificationManager from './GleapNotificationManager'; // GleapTabCommunication, GleapSession and saveToGleapCache. jest.mock('./Gleap', () => ({ __esModule: true, - default: {}, + default: { getInstance: jest.fn() }, GleapFeedbackButtonManager: { getInstance: jest.fn(() => ({ updateNotificationBadge: jest.fn() })) }, GleapConfigManager: { getInstance: jest.fn(() => ({ getFlowConfig: () => ({}) })) }, GleapSession: { getInstance: jest.fn() }, @@ -44,6 +44,7 @@ const withContainer = (nm) => { }; beforeEach(() => { + Gleap.getInstance.mockReturnValue({ disableInAppNotifications: false }); sendMessageMock = jest.fn(); GleapTabCommunication.getInstance.mockReturnValue({ sendMessage: sendMessageMock }); GleapSession.getInstance.mockReturnValue({ session: { gleapId: 'user-1' } }); diff --git a/src/GleapNotifications.disabled.test.js b/src/GleapNotifications.disabled.test.js new file mode 100644 index 00000000..26e21f1b --- /dev/null +++ b/src/GleapNotifications.disabled.test.js @@ -0,0 +1,125 @@ +/** + * @jest-environment jsdom + */ +import Gleap, { + GleapAudioManager, + GleapConfigManager, + GleapFeedbackButtonManager, + GleapFrameManager, + GleapNotificationManager, + GleapSession, +} from './Gleap'; +import * as cache from './GleapHelper'; + +const news = (extra = {}) => ({ + outbound: 'product-announcement', + createdAt: new Date().toISOString(), + cachedForGleapId: 'signup-visitor', + data: { news: { id: 'news-1' }, text: 'Product announcement' }, + ...extra, +}); + +const mountNotifications = () => { + const manager = GleapNotificationManager.getInstance(); + manager.notificationContainer = document.createElement('div'); + document.body.appendChild(manager.notificationContainer); + return manager; +}; + +const visibleCards = (manager) => manager.notificationContainer.querySelectorAll('.gleap-notification-card'); + +beforeEach(() => { + // Exercise the real public setter and notification renderer, without SDK startup. + jest.spyOn(Gleap, 'getInstance').mockReturnValue({ disableInAppNotifications: false }); + jest.spyOn(GleapSession, 'getInstance').mockReturnValue({ + session: { gleapId: 'signup-visitor' }, + getName: () => 'Visitor', + }); + jest.spyOn(GleapConfigManager, 'getInstance').mockReturnValue({ getFlowConfig: () => ({}) }); + jest.spyOn(GleapAudioManager, 'ping').mockImplementation(() => {}); + jest.spyOn(cache, 'loadFromGleapCache').mockReturnValue(null); + jest.spyOn(cache, 'saveToGleapCache').mockImplementation(() => {}); + GleapNotificationManager.instance = undefined; +}); + +afterEach(() => { + jest.restoreAllMocks(); + GleapNotificationManager.instance = undefined; + document.body.innerHTML = ''; +}); + +test('disabling before startup keeps cached announcements hidden without deleting them', () => { + Gleap.setDisableInAppNotifications(true); + const manager = mountNotifications(); + const cached = news(); + cache.loadFromGleapCache.mockReturnValue([cached]); + + manager.reloadNotificationsFromCache(); + + expect(visibleCards(manager)).toHaveLength(0); + expect(manager.notifications).toEqual([cached]); + expect(cache.saveToGleapCache).not.toHaveBeenCalled(); + + Gleap.setDisableInAppNotifications(false); + expect(visibleCards(manager)).toHaveLength(1); +}); + +test('disabling immediately hides an existing card and keeps it hidden on cache reload', () => { + const manager = mountNotifications(); + const cached = news(); + cache.loadFromGleapCache.mockReturnValue([cached]); + manager.reloadNotificationsFromCache(); + expect(visibleCards(manager)).toHaveLength(1); + + Gleap.setDisableInAppNotifications(true); + expect(manager.notificationContainer.children).toHaveLength(0); + + // Closing the messenger reloads cached notifications through the same path. + manager.reloadNotificationsFromCache(); + expect(manager.notificationContainer.children).toHaveLength(0); + expect(manager.notifications).toEqual([cached]); + + Gleap.setDisableInAppNotifications(false); + expect(visibleCards(manager)).toHaveLength(1); + expect(cache.saveToGleapCache).not.toHaveBeenCalled(); +}); + +test('a directly delivered notification stays silent and hidden while disabled', () => { + const manager = mountNotifications(); + Gleap.setDisableInAppNotifications(true); + + Gleap.showNotification(news({ sound: true })); + + expect(visibleCards(manager)).toHaveLength(0); + expect(GleapAudioManager.ping).not.toHaveBeenCalled(); + expect(manager.notifications).toHaveLength(1); + + Gleap.setDisableInAppNotifications(false); + expect(visibleCards(manager)).toHaveLength(1); + expect(GleapAudioManager.ping).not.toHaveBeenCalled(); +}); + +test('reenabling notifications still respects their page exclusions', () => { + const manager = mountNotifications(); + manager.notifications = [news({ pageRules: [{ pageFilter: '/', pageFilterType: 'notcontains' }] })]; + + Gleap.setDisableInAppNotifications(true); + Gleap.setDisableInAppNotifications(false); + + expect(visibleCards(manager)).toHaveLength(0); + expect(manager.notifications).toHaveLength(1); +}); + +test('support chat can still open while notification cards are disabled', () => { + const frame = { setAppMode: jest.fn(), showWidget: jest.fn(), hideWidget: jest.fn() }; + jest.spyOn(GleapFrameManager, 'getInstance').mockReturnValue(frame); + const buttonManager = jest.spyOn(GleapFeedbackButtonManager, 'getInstance'); + + Gleap.setDisableInAppNotifications(true); + Gleap.open(); + + expect(frame.setAppMode).toHaveBeenCalledWith('widget'); + expect(frame.showWidget).toHaveBeenCalledTimes(1); + expect(frame.hideWidget).not.toHaveBeenCalled(); + expect(buttonManager).not.toHaveBeenCalled(); +});