diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index 415cc361926..5bf2507194e 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -219,6 +219,18 @@ describe("BrowserApi", () => { window.location.reload = reload; }); + it("skips reloading any windows if no views can be found", () => { + Object.defineProperty(window, "location", { + value: { reload: jest.fn(), href: "chrome-extension://id-value/background.html" }, + writable: true, + }); + chrome.extension.getViews = jest.fn().mockReturnValue([]); + + BrowserApi.reloadOpenWindows(); + + expect(window.location.reload).not.toHaveBeenCalled(); + }); + it("reloads all open windows", () => { Object.defineProperty(window, "location", { value: { reload: jest.fn(), href: "chrome-extension://id-value/index.html" }, diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b252b05421d..c65105d3840 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -404,8 +404,12 @@ export class BrowserApi { * @param exemptCurrentHref - Whether to exempt the current window location from the reload. */ static reloadOpenWindows(exemptCurrentHref = false) { - const currentHref = window?.location.href; const views = BrowserApi.getExtensionViews(); + if (!views.length) { + return; + } + + const currentHref = window.location.href; views .filter((w) => w.location.href != null && !w.location.href.includes("background.html")) .filter((w) => !exemptCurrentHref || w.location.href !== currentHref) diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts index 11afabed643..f9687d58662 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/browser-platform-utils.service.spec.ts @@ -1,5 +1,7 @@ import { DeviceType } from "@bitwarden/common/enums"; +import { BrowserApi } from "../browser/browser-api"; + import BrowserPlatformUtilsService from "./browser-platform-utils.service"; describe("Browser Utils Service", () => { @@ -91,41 +93,24 @@ describe("Browser Utils Service", () => { }); describe("isViewOpen", () => { - beforeEach(() => { - globalThis.chrome = { - // eslint-disable-next-line - // @ts-ignore - extension: { - getViews: jest.fn(), - }, - }; + it("returns false if a heartbeat response is not received", async () => { + BrowserApi.sendMessageWithResponse = jest.fn().mockResolvedValueOnce(undefined); + + const isViewOpen = await browserPlatformUtilsService.isViewOpen(); + + expect(isViewOpen).toBe(false); }); - it("returns true if the user is on Firefox and the sidebar is open", async () => { - chrome.extension.getViews = jest.fn().mockReturnValueOnce([window]); - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValueOnce(DeviceType.FirefoxExtension); + it("returns true if a heartbeat response is received", async () => { + BrowserApi.sendMessageWithResponse = jest + .fn() + .mockImplementationOnce((subscriber) => + Promise.resolve((subscriber === "checkVaultPopupHeartbeat") as any), + ); - const result = await browserPlatformUtilsService.isViewOpen(); + const isViewOpen = await browserPlatformUtilsService.isViewOpen(); - expect(result).toBe(true); - }); - - it("returns true if a extension view is open as a tab", async () => { - chrome.extension.getViews = jest.fn().mockReturnValueOnce([window]); - - const result = await browserPlatformUtilsService.isViewOpen(); - - expect(result).toBe(true); - }); - - it("returns false if no extension view is open", async () => { - chrome.extension.getViews = jest.fn().mockReturnValue([]); - - const result = await browserPlatformUtilsService.isViewOpen(); - - expect(result).toBe(false); + expect(isViewOpen).toBe(true); }); }); }); diff --git a/apps/browser/src/platform/services/browser-platform-utils.service.ts b/apps/browser/src/platform/services/browser-platform-utils.service.ts index 63f5b0345c7..c3ce8adda1d 100644 --- a/apps/browser/src/platform/services/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/browser-platform-utils.service.ts @@ -150,23 +150,13 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return false; } + /** + * Identifies if the vault popup is currently open. This is done by sending a + * message to the popup and waiting for a response. If a response is received, + * the view is open. + */ async isViewOpen(): Promise { - if (await BrowserApi.isPopupOpen()) { - return true; - } - - if (this.isSafari()) { - return false; - } - - // Opera has "sidebar_panel" as a ViewType but doesn't currently work - if (this.isFirefox() && BrowserApi.getExtensionViews({ type: "sidebar" }).length > 0) { - return true; - } - - // Opera sidebar has type of "tab" (will stick around for a while after closing sidebar) - const tabOpen = BrowserApi.getExtensionViews({ type: "tab" }).length > 0; - return tabOpen; + return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat")); } lockTimeout(): number { diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 005664bdd20..aeb57858450 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,6 +6,7 @@ import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; @@ -52,6 +53,25 @@ export class InitService { } this.configService.init(); + this.setupVaultPopupHeartbeat(); }; } + + /** + * Sets up a runtime message listener to indicate to the background + * script that the extension popup is open in some manner. + */ + private setupVaultPopupHeartbeat() { + const respondToHeartbeat = ( + message: { command: string }, + _sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + if (message?.command === "checkVaultPopupHeartbeat") { + sendResponse(true); + } + }; + + BrowserApi.messageListener("vaultPopupHeartbeat", respondToHeartbeat); + } }