mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-3914] Refactor Browser Extension Popout Windows (#6296)
* [PM-3914] Refactor Browser Extension Popouts * [PM-3914] Refactor Browser Extension Popouts * [PM-3914] Refactor Browser Extension Popouts * [PM-3914] Adding enums for the browser popout type * [PM-3914] Making the methods for getting a window in a targeted manner public * [PM-3914] Refactoing implementation * [PM-3914] Updating deprecated api call * [PM-3914] Fixing issues found when testing behavior * [PM-3914] Reimplementing behavior based on feedback from platform team * [PM-3914] Adding method of ensuring previously opened single action window is force closed for vault item password reprompts * [PM-3914] Taking into consideration feedback regarding the browser popup utils service and implementating requested changes * [PM-3914] Removing unnecesssary class dependencies * [PM-3914] Adding method for uniquely setting up password reprompt windows * [PM-3914] Modifying method * [PM-3914] Adding jest tests and documentation for AuthPopoutWindow util * [PM-3914] Adding jest tests and documentation for VaultPopoutWindow * [PM-3914] Adding jest tests for the debouncing method within autofill service * [PM-3914] Adding jest tests for the new BrowserApi methods * [PM-3914] Adding jest tests to the BrowserPopupUtils class * [PM-3914] Updating inPrivateMode reference * [PM-3914] Updating inPrivateMode reference * [PM-3914] Modifying comment * [PM-3914] Moviing implementation for openCurrentPagePopout to the BrowserPopupUtils * [PM-3914] Applying feedback * [PM-3914] Applying feedback * [PM-3914] Applying feedback * [PM-3983] Refactoring implementation of `setContentScrollY` to facilitate having a potential delay * [PM-3914] Applying feedback regarding setContentScrollY to the implementation * [PM-3914] Modifying early return within the run method of the ContextMenuClickedHandler * [PM-3914] Adding test for VaultPopoutWindow * [PM-3914] Applying work done within PM-4366 to facilitate opening the popout window as a popup rather than a normal window * [PM-3914] Updating the BrowserApi.removeTab method to leverage a callback structure for the promise rather than an async away structure * [PM-3036] Adding jest tests for added passkeys popout windows * [PM-3914] Adjsuting logic for turning off the warning when FIDO2 credentials are saved * [PM-3914] Fixing height to design * [PM-3914] Fixing call to Fido2 Popout * [PM-3914] Fixing add/edit from fido2 popout * [PM-3914] Fixing add/edit from fido2 popout * [PM-3914] Fixing jest tests for updated elements * [PM-3914] Reverting how context menu actions are passed to the view component * [PM-3914] Reverting re-instantiation of config service within main.background.ts * [PM-3914] Adding jest test for BrowserAPI removeTab method * [PM-3914] Adding method to handle parsing the popout url path * [PM-3914] Removing JSDOC comment elements * [PM-3914] Removing await from method call * [PM-3914] Simplifying implementation on add/edit * [PM-3032] Adding more direct reference to view item action in context menus * [PM-3914] Adjusting routing on Fido2 component to pass the singleActionPopout param to the route when opening the add-edit component * [PM-3914] Adding singleActionPopout param to the fido2 component routing * [PM-3914] Updating implementation details for how we build the extension url path * [PM-3914] Reworking implementation for isSingleActionPopoutOpen to clean up iterative logic * [PM-3914] Merging work from master and fixing merge conflicts * [PM-3914] Fixing merge conflict introduced from master * [PM-3914] Reworking closure of single action popouts to ensure they close the window instead of attempting to close the tab * [PM-3914] Fixing issue within Opera where lock and login routes can persist if user opens the extension popout in a new window before locking or logging out * [PM-3914] Setting the extensionUrls that are cheked as a variable outside of the scope fo the openUlockPopout method to ensure it does not have to be rebuilt each time the method is called
This commit is contained in:
parent
16c567ab59
commit
cf6ada531e
@ -22,7 +22,9 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { PopupUtilsService } from "../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
|
||||
import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window";
|
||||
|
||||
const BroadcasterSubscriptionId = "TwoFactorComponent";
|
||||
|
||||
@ -31,8 +33,6 @@ const BroadcasterSubscriptionId = "TwoFactorComponent";
|
||||
templateUrl: "two-factor.component.html",
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
showNewWindowMessage = false;
|
||||
|
||||
constructor(
|
||||
authService: AuthService,
|
||||
router: Router,
|
||||
@ -42,7 +42,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
private syncService: SyncService,
|
||||
environmentService: EnvironmentService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
stateService: StateService,
|
||||
route: ActivatedRoute,
|
||||
private messagingService: MessagingService,
|
||||
@ -115,7 +114,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
|
||||
if (
|
||||
this.selectedProviderType === TwoFactorProviderType.Email &&
|
||||
this.popupUtilsService.inPopup(window)
|
||||
BrowserPopupUtils.inPopup(window)
|
||||
) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
@ -123,7 +122,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
type: "warning",
|
||||
});
|
||||
if (confirmed) {
|
||||
this.popupUtilsService.popOut(window);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,7 +141,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
|
||||
// We don't need this window anymore because the intent is for the user to be left
|
||||
// on the web vault screen which tells them to continue in the browser extension (sidebar or popup)
|
||||
BrowserApi.closeBitwardenExtensionTab();
|
||||
await closeTwoFactorAuthPopout();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
103
apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts
Normal file
103
apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { createChromeTabMock } from "../../../autofill/jest/autofill-mocks";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
import {
|
||||
AuthPopoutType,
|
||||
openUnlockPopout,
|
||||
closeUnlockPopout,
|
||||
openSsoAuthResultPopout,
|
||||
openTwoFactorAuthPopout,
|
||||
closeTwoFactorAuthPopout,
|
||||
} from "./auth-popout-window";
|
||||
|
||||
describe("AuthPopoutWindow", () => {
|
||||
const openPopoutSpy = jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation();
|
||||
const sendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
|
||||
const closeSingleActionPopoutSpy = jest
|
||||
.spyOn(BrowserPopupUtils, "closeSingleActionPopout")
|
||||
.mockImplementation();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("openUnlockPopout", () => {
|
||||
it("opens a single action popup that allows the user to unlock the extension and sends a `bgUnlockPopoutOpened` message", async () => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]);
|
||||
const senderTab = { windowId: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await openUnlockPopout(senderTab);
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html", {
|
||||
singleActionKey: AuthPopoutType.unlockExtension,
|
||||
senderWindowId: 1,
|
||||
});
|
||||
expect(sendMessageDataSpy).toHaveBeenCalledWith(senderTab, "bgUnlockPopoutOpened");
|
||||
});
|
||||
|
||||
it("closes any existing popup window types that are open to the unlock extension route", async () => {
|
||||
const unlockTab = createChromeTabMock({
|
||||
url: chrome.runtime.getURL("popup/index.html#/lock"),
|
||||
});
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([unlockTab]);
|
||||
jest.spyOn(BrowserApi, "removeWindow");
|
||||
const senderTab = { windowId: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await openUnlockPopout(senderTab);
|
||||
|
||||
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({ windowType: "popup" });
|
||||
expect(BrowserApi.removeWindow).toHaveBeenCalledWith(unlockTab.windowId);
|
||||
});
|
||||
|
||||
it("closes any existing popup window types that are open to the login extension route", async () => {
|
||||
const loginTab = createChromeTabMock({
|
||||
url: chrome.runtime.getURL("popup/index.html#/home"),
|
||||
});
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([loginTab]);
|
||||
jest.spyOn(BrowserApi, "removeWindow");
|
||||
const senderTab = { windowId: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await openUnlockPopout(senderTab);
|
||||
|
||||
expect(BrowserApi.removeWindow).toHaveBeenCalledWith(loginTab.windowId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeUnlockPopout", () => {
|
||||
it("closes the unlock extension popout window", () => {
|
||||
closeUnlockPopout();
|
||||
|
||||
expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith("auth_unlockExtension");
|
||||
});
|
||||
});
|
||||
|
||||
describe("openSsoAuthResultPopout", () => {
|
||||
it("opens a window that facilitates presentation of the results for SSO authentication", () => {
|
||||
openSsoAuthResultPopout({ code: "code", state: "state" });
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/sso?code=code&state=state", {
|
||||
singleActionKey: AuthPopoutType.ssoAuthResult,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openTwoFactorAuthPopout", () => {
|
||||
it("opens a window that facilitates two factor authentication", () => {
|
||||
openTwoFactorAuthPopout({ data: "data", remember: "remember" });
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/2fa;webAuthnResponse=data;remember=remember",
|
||||
{ singleActionKey: AuthPopoutType.twoFactorAuth }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeTwoFactorAuthPopout", () => {
|
||||
it("closes the two-factor authentication window", () => {
|
||||
closeTwoFactorAuthPopout();
|
||||
|
||||
expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith("auth_twoFactorAuth");
|
||||
});
|
||||
});
|
||||
});
|
87
apps/browser/src/auth/popup/utils/auth-popout-window.ts
Normal file
87
apps/browser/src/auth/popup/utils/auth-popout-window.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
const AuthPopoutType = {
|
||||
unlockExtension: "auth_unlockExtension",
|
||||
ssoAuthResult: "auth_ssoAuthResult",
|
||||
twoFactorAuth: "auth_twoFactorAuth",
|
||||
} as const;
|
||||
const extensionUnlockUrls = new Set([
|
||||
chrome.runtime.getURL("popup/index.html#/lock"),
|
||||
chrome.runtime.getURL("popup/index.html#/home"),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Opens a window that facilitates unlocking / logging into the extension.
|
||||
*
|
||||
* @param senderTab - Used to determine the windowId of the sender.
|
||||
*/
|
||||
async function openUnlockPopout(senderTab: chrome.tabs.Tab) {
|
||||
const existingPopoutWindowTabs = await BrowserApi.tabsQuery({ windowType: "popup" });
|
||||
existingPopoutWindowTabs.forEach((tab) => {
|
||||
if (extensionUnlockUrls.has(tab.url)) {
|
||||
BrowserApi.removeWindow(tab.windowId);
|
||||
}
|
||||
});
|
||||
|
||||
await BrowserPopupUtils.openPopout("popup/index.html", {
|
||||
singleActionKey: AuthPopoutType.unlockExtension,
|
||||
senderWindowId: senderTab.windowId,
|
||||
});
|
||||
await BrowserApi.tabSendMessageData(senderTab, "bgUnlockPopoutOpened");
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the unlock popout window.
|
||||
*/
|
||||
async function closeUnlockPopout() {
|
||||
await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.unlockExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a window that facilitates presenting the results for SSO authentication.
|
||||
*
|
||||
* @param resultData - The result data from the SSO authentication.
|
||||
*/
|
||||
async function openSsoAuthResultPopout(resultData: { code: string; state: string }) {
|
||||
const { code, state } = resultData;
|
||||
const authResultUrl = `popup/index.html#/sso?code=${encodeURIComponent(
|
||||
code
|
||||
)}&state=${encodeURIComponent(state)}`;
|
||||
|
||||
await BrowserPopupUtils.openPopout(authResultUrl, {
|
||||
singleActionKey: AuthPopoutType.ssoAuthResult,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a window that facilitates two-factor authentication.
|
||||
*
|
||||
* @param twoFactorAuthData - The data from the two-factor authentication.
|
||||
*/
|
||||
async function openTwoFactorAuthPopout(twoFactorAuthData: { data: string; remember: string }) {
|
||||
const { data, remember } = twoFactorAuthData;
|
||||
const params =
|
||||
`webAuthnResponse=${encodeURIComponent(data)};` + `remember=${encodeURIComponent(remember)}`;
|
||||
const twoFactorUrl = `popup/index.html#/2fa;${params}`;
|
||||
|
||||
await BrowserPopupUtils.openPopout(twoFactorUrl, {
|
||||
singleActionKey: AuthPopoutType.twoFactorAuth,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the two-factor authentication popout window.
|
||||
*/
|
||||
async function closeTwoFactorAuthPopout() {
|
||||
await BrowserPopupUtils.closeSingleActionPopout(AuthPopoutType.twoFactorAuth);
|
||||
}
|
||||
|
||||
export {
|
||||
AuthPopoutType,
|
||||
openUnlockPopout,
|
||||
closeUnlockPopout,
|
||||
openSsoAuthResultPopout,
|
||||
openTwoFactorAuthPopout,
|
||||
closeTwoFactorAuthPopout,
|
||||
};
|
@ -12,6 +12,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||
import AddUnlockVaultQueueMessage from "../../background/models/add-unlock-vault-queue-message";
|
||||
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
|
||||
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
|
||||
@ -21,6 +22,7 @@ import LockedVaultPendingNotificationsItem from "../../background/models/lockedV
|
||||
import { NotificationQueueMessageType } from "../../background/models/notificationQueueMessageType";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
|
||||
export default class NotificationBackground {
|
||||
@ -96,7 +98,7 @@ export default class NotificationBackground {
|
||||
"addToLockedVaultPendingNotifications",
|
||||
retryMessage
|
||||
);
|
||||
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
|
||||
await openUnlockPopout(sender.tab);
|
||||
return;
|
||||
}
|
||||
await this.saveOrUpdateCredentials(sender.tab, msg.edit, msg.folder);
|
||||
@ -118,9 +120,12 @@ export default class NotificationBackground {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "promptForLogin":
|
||||
case "bgUnlockPopoutOpened":
|
||||
await this.unlockVault(sender.tab);
|
||||
break;
|
||||
case "bgReopenUnlockPopout":
|
||||
await openUnlockPopout(sender.tab);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -447,9 +452,7 @@ export default class NotificationBackground {
|
||||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
||||
await BrowserApi.tabSendMessageData(senderTab, "openAddEditCipher", {
|
||||
cipherId: cipherView.id,
|
||||
});
|
||||
await openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
|
||||
}
|
||||
|
||||
private async folderExists(folderId: string) {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
} from "../../auth/background/service-factories/auth-service.factory";
|
||||
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
|
||||
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||
import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
|
||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||
import { Account } from "../../models/account";
|
||||
@ -29,6 +30,10 @@ import {
|
||||
cipherServiceFactory,
|
||||
CipherServiceInitOptions,
|
||||
} from "../../vault/background/service_factories/cipher-service.factory";
|
||||
import {
|
||||
openAddEditVaultItemPopout,
|
||||
openVaultItemPasswordRepromptPopout,
|
||||
} from "../../vault/popup/utils/vault-popout-window";
|
||||
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
||||
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
||||
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
||||
@ -187,7 +192,7 @@ export class ContextMenuClickedHandler {
|
||||
retryMessage
|
||||
);
|
||||
|
||||
await BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
||||
await openUnlockPopout(tab);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -235,14 +240,12 @@ export class ContextMenuClickedHandler {
|
||||
const cipherType = this.getCipherCreationType(menuItemId);
|
||||
|
||||
if (cipherType) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType,
|
||||
});
|
||||
await openAddEditVaultItemPopout(tab, { cipherType });
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
await openVaultItemPasswordRepromptPopout(tab, {
|
||||
cipherId: cipher.id,
|
||||
// The action here is passed on to the single-use reprompt window and doesn't change based on cipher type
|
||||
action: AUTOFILL_ID,
|
||||
@ -255,9 +258,7 @@ export class ContextMenuClickedHandler {
|
||||
}
|
||||
case COPY_USERNAME_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||
break;
|
||||
}
|
||||
|
||||
@ -265,16 +266,14 @@ export class ContextMenuClickedHandler {
|
||||
break;
|
||||
case COPY_PASSWORD_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
await openVaultItemPasswordRepromptPopout(tab, {
|
||||
cipherId: cipher.id,
|
||||
action: info.parentMenuItemId,
|
||||
action: COPY_PASSWORD_ID,
|
||||
});
|
||||
} else {
|
||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||
@ -284,16 +283,14 @@ export class ContextMenuClickedHandler {
|
||||
break;
|
||||
case COPY_VERIFICATIONCODE_ID:
|
||||
if (menuItemId === CREATE_LOGIN_ID) {
|
||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
||||
cipherType: CipherType.Login,
|
||||
});
|
||||
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||
break;
|
||||
}
|
||||
|
||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
await openVaultItemPasswordRepromptPopout(tab, {
|
||||
cipherId: cipher.id,
|
||||
action: info.parentMenuItemId,
|
||||
action: COPY_VERIFICATIONCODE_ID,
|
||||
});
|
||||
} else {
|
||||
this.copyToClipboard({
|
||||
|
@ -28,12 +28,10 @@ window.addEventListener(
|
||||
);
|
||||
|
||||
const forwardCommands = [
|
||||
"promptForLogin",
|
||||
"passwordReprompt",
|
||||
"bgUnlockPopoutOpened",
|
||||
"addToLockedVaultPendingNotifications",
|
||||
"unlockCompleted",
|
||||
"addedCipher",
|
||||
"openAddEditCipher",
|
||||
];
|
||||
|
||||
chrome.runtime.onMessage.addListener((event) => {
|
||||
|
@ -189,7 +189,7 @@ function handleTypeUnlock() {
|
||||
const unlockButton = document.getElementById("unlock-vault");
|
||||
unlockButton.addEventListener("click", (e) => {
|
||||
sendPlatformMessage({
|
||||
command: "bgReopenPromptForLogin",
|
||||
command: "bgReopenUnlockPopout",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -752,13 +752,15 @@ describe("AutofillService", () => {
|
||||
jest
|
||||
.spyOn(userVerificationService, "hasMasterPasswordAndMasterKeyHash")
|
||||
.mockResolvedValueOnce(true);
|
||||
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
|
||||
jest
|
||||
.spyOn(autofillService as any, "openVaultItemPasswordRepromptPopout")
|
||||
.mockImplementation();
|
||||
|
||||
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
|
||||
|
||||
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
|
||||
expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
|
||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(tab, "passwordReprompt", {
|
||||
expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, {
|
||||
cipherId: cipher.id,
|
||||
action: "autofill",
|
||||
});
|
||||
@ -4254,4 +4256,24 @@ describe("AutofillService", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDebouncingPasswordRepromptPopout", () => {
|
||||
it("returns false and sets up the debounce if a master password reprompt window is not currently opening", () => {
|
||||
jest.spyOn(globalThis, "setTimeout");
|
||||
|
||||
const result = autofillService["isDebouncingPasswordRepromptPopout"]();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 100);
|
||||
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true if a master password reprompt window is currently opening", () => {
|
||||
autofillService["currentlyOpeningPasswordRepromptPopout"] = true;
|
||||
|
||||
const result = autofillService["isDebouncingPasswordRepromptPopout"]();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import AutofillScript from "../models/autofill-script";
|
||||
@ -30,6 +31,10 @@ import {
|
||||
} from "./autofill-constants";
|
||||
|
||||
export default class AutofillService implements AutofillServiceInterface {
|
||||
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
|
||||
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
|
||||
private currentlyOpeningPasswordRepromptPopout = false;
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private stateService: BrowserStateService,
|
||||
@ -272,13 +277,14 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
if (
|
||||
cipher.reprompt === CipherRepromptType.Password &&
|
||||
// If the master password has is not available, reprompt will error
|
||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) &&
|
||||
!this.isDebouncingPasswordRepromptPopout()
|
||||
) {
|
||||
if (fromCommand) {
|
||||
this.cipherService.updateLastUsedIndexForUrl(tab.url);
|
||||
}
|
||||
|
||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
||||
await this.openVaultItemPasswordRepromptPopout(tab, {
|
||||
cipherId: cipher.id,
|
||||
action: "autofill",
|
||||
});
|
||||
@ -1828,4 +1834,22 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
static forCustomFieldsOnly(field: AutofillField): boolean {
|
||||
return field.tagName === "span";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles debouncing the opening of the master password reprompt popout.
|
||||
*/
|
||||
private isDebouncingPasswordRepromptPopout() {
|
||||
if (this.currentlyOpeningPasswordRepromptPopout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.currentlyOpeningPasswordRepromptPopout = true;
|
||||
clearTimeout(this.openPasswordRepromptPopoutDebounce);
|
||||
|
||||
this.openPasswordRepromptPopoutDebounce = setTimeout(() => {
|
||||
this.currentlyOpeningPasswordRepromptPopout = false;
|
||||
}, 100);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { openUnlockPopout } from "../auth/popup/utils/auth-popout-window";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
|
||||
import MainBackground from "./main.background";
|
||||
@ -87,7 +88,7 @@ export default class CommandsBackground {
|
||||
retryMessage
|
||||
);
|
||||
|
||||
BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
||||
await openUnlockPopout(tab);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,6 @@ import { Account } from "../models/account";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||
import BrowserPopoutWindowService from "../platform/popup/browser-popout-window.service";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserConfigService } from "../platform/services/browser-config.service";
|
||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||
@ -145,7 +144,6 @@ import BrowserPlatformUtilsService from "../platform/services/browser-platform-u
|
||||
import { BrowserStateService } from "../platform/services/browser-state.service";
|
||||
import { KeyGenerationService } from "../platform/services/key-generation.service";
|
||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
||||
import { PopupUtilsService } from "../popup/services/popup-utils.service";
|
||||
import { BrowserSendService } from "../services/browser-send.service";
|
||||
import { BrowserSettingsService } from "../services/browser-settings.service";
|
||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
||||
@ -225,8 +223,6 @@ export default class MainBackground {
|
||||
devicesService: DevicesServiceAbstraction;
|
||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
||||
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
|
||||
popupUtilsService: PopupUtilsService;
|
||||
browserPopoutWindowService: BrowserPopoutWindowService;
|
||||
accountService: AccountServiceAbstraction;
|
||||
|
||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||
@ -583,12 +579,7 @@ export default class MainBackground {
|
||||
this.messagingService
|
||||
);
|
||||
|
||||
this.browserPopoutWindowService = new BrowserPopoutWindowService();
|
||||
|
||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(
|
||||
this.browserPopoutWindowService,
|
||||
this.authService
|
||||
);
|
||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
||||
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
|
||||
this.cipherService,
|
||||
this.fido2UserInterfaceService,
|
||||
@ -634,8 +625,7 @@ export default class MainBackground {
|
||||
this.environmentService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.browserPopoutWindowService
|
||||
this.configService
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.cryptoService,
|
||||
|
@ -8,9 +8,13 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import {
|
||||
closeUnlockPopout,
|
||||
openSsoAuthResultPopout,
|
||||
openTwoFactorAuthPopout,
|
||||
} from "../auth/popup/utils/auth-popout-window";
|
||||
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { BrowserPopoutWindowService } from "../platform/popup/abstractions/browser-popout-window.service";
|
||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
||||
@ -37,8 +41,7 @@ export default class RuntimeBackground {
|
||||
private environmentService: BrowserEnvironmentService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private browserPopoutWindowService: BrowserPopoutWindowService
|
||||
private configService: ConfigServiceAbstraction
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
@ -82,8 +85,6 @@ export default class RuntimeBackground {
|
||||
}
|
||||
|
||||
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
|
||||
const cipherId = msg.data?.cipherId;
|
||||
|
||||
switch (msg.command) {
|
||||
case "loggedIn":
|
||||
case "unlocked": {
|
||||
@ -91,7 +92,7 @@ export default class RuntimeBackground {
|
||||
|
||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||
item = this.lockedVaultPendingNotifications.pop();
|
||||
await this.browserPopoutWindowService.closeUnlockPrompt();
|
||||
await closeUnlockPopout();
|
||||
}
|
||||
|
||||
await this.main.refreshBadge();
|
||||
@ -129,49 +130,6 @@ export default class RuntimeBackground {
|
||||
case "openPopup":
|
||||
await this.main.openPopup();
|
||||
break;
|
||||
case "promptForLogin":
|
||||
case "bgReopenPromptForLogin":
|
||||
await this.browserPopoutWindowService.openUnlockPrompt(sender.tab?.windowId);
|
||||
break;
|
||||
case "passwordReprompt":
|
||||
if (cipherId) {
|
||||
await this.browserPopoutWindowService.openPasswordRepromptPrompt(sender.tab?.windowId, {
|
||||
cipherId: cipherId,
|
||||
senderTabId: sender.tab.id,
|
||||
action: msg.data?.action,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "openAddEditCipher": {
|
||||
const isNewCipher = !cipherId;
|
||||
const cipherType = msg.data?.cipherType;
|
||||
const senderTab = sender.tab;
|
||||
|
||||
if (!senderTab) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isNewCipher) {
|
||||
await this.browserPopoutWindowService.openCipherCreation(senderTab.windowId, {
|
||||
cipherType,
|
||||
senderTabId: senderTab.id,
|
||||
senderTabURI: senderTab.url,
|
||||
});
|
||||
} else {
|
||||
await this.browserPopoutWindowService.openCipherEdit(senderTab.windowId, {
|
||||
cipherId,
|
||||
senderTabId: senderTab.id,
|
||||
senderTabURI: senderTab.url,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "closeTab":
|
||||
setTimeout(() => {
|
||||
BrowserApi.closeBitwardenExtensionTab();
|
||||
}, msg.delay ?? 0);
|
||||
break;
|
||||
case "triggerAutofillScriptInjection":
|
||||
await this.autofillService.injectAutofillScripts(
|
||||
sender,
|
||||
@ -266,12 +224,7 @@ export default class RuntimeBackground {
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
BrowserApi.createNewTab(
|
||||
"popup/index.html?uilocation=popout#/sso?code=" +
|
||||
encodeURIComponent(msg.code) +
|
||||
"&state=" +
|
||||
encodeURIComponent(msg.state)
|
||||
);
|
||||
await openSsoAuthResultPopout(msg);
|
||||
} catch {
|
||||
this.logService.error("Unable to open sso popout tab");
|
||||
}
|
||||
@ -285,10 +238,7 @@ export default class RuntimeBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
const params =
|
||||
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
|
||||
`remember=${encodeURIComponent(msg.remember)}`;
|
||||
BrowserApi.openBitwardenExtensionTab(`popup/index.html#/2fa;${params}`, false);
|
||||
await openTwoFactorAuthPopout(msg);
|
||||
break;
|
||||
}
|
||||
case "reloadPopup":
|
||||
|
@ -9,6 +9,89 @@ describe("BrowserApi", () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getWindow", () => {
|
||||
it("will get the current window if a window id is not provided", () => {
|
||||
BrowserApi.getWindow();
|
||||
|
||||
expect(chrome.windows.getCurrent).toHaveBeenCalledWith({ populate: true }, expect.anything());
|
||||
});
|
||||
|
||||
it("will get the window with the provided id if one is provided", () => {
|
||||
const windowId = 1;
|
||||
|
||||
BrowserApi.getWindow(windowId);
|
||||
|
||||
expect(chrome.windows.get).toHaveBeenCalledWith(
|
||||
windowId,
|
||||
{ populate: true },
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurrentWindow", () => {
|
||||
it("will get the current window", () => {
|
||||
BrowserApi.getCurrentWindow();
|
||||
|
||||
expect(chrome.windows.getCurrent).toHaveBeenCalledWith({ populate: true }, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWindowById", () => {
|
||||
it("will get the window associated with the passed window id", () => {
|
||||
const windowId = 1;
|
||||
|
||||
BrowserApi.getWindowById(windowId);
|
||||
|
||||
expect(chrome.windows.get).toHaveBeenCalledWith(
|
||||
windowId,
|
||||
{ populate: true },
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeWindow", () => {
|
||||
it("removes the window based on the passed window id", () => {
|
||||
const windowId = 10;
|
||||
|
||||
BrowserApi.removeWindow(windowId);
|
||||
|
||||
expect(chrome.windows.remove).toHaveBeenCalledWith(windowId, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateWindowProperties", () => {
|
||||
it("will update the window with the provided window options", () => {
|
||||
const windowId = 1;
|
||||
const windowOptions: chrome.windows.UpdateInfo = {
|
||||
focused: true,
|
||||
};
|
||||
|
||||
BrowserApi.updateWindowProperties(windowId, windowOptions);
|
||||
|
||||
expect(chrome.windows.update).toHaveBeenCalledWith(
|
||||
windowId,
|
||||
windowOptions,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("focusWindow", () => {
|
||||
it("will focus the window with the provided window id", () => {
|
||||
const windowId = 1;
|
||||
|
||||
BrowserApi.focusWindow(windowId);
|
||||
|
||||
expect(chrome.windows.update).toHaveBeenCalledWith(
|
||||
windowId,
|
||||
{ focused: true },
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executeScriptInTab", () => {
|
||||
it("calls to the extension api to execute a script within the give tabId", async () => {
|
||||
const tabId = 1;
|
||||
|
@ -19,14 +19,33 @@ export class BrowserApi {
|
||||
return chrome.runtime.getManifest().manifest_version;
|
||||
}
|
||||
|
||||
static getWindow(windowId?: number): Promise<chrome.windows.Window> | void {
|
||||
/**
|
||||
* Gets the current window or the window with the given id.
|
||||
*
|
||||
* @param windowId - The id of the window to get. If not provided, the current window is returned.
|
||||
*/
|
||||
static async getWindow(windowId?: number): Promise<chrome.windows.Window> {
|
||||
if (!windowId) {
|
||||
return;
|
||||
return BrowserApi.getCurrentWindow();
|
||||
}
|
||||
|
||||
return new Promise((resolve) =>
|
||||
chrome.windows.get(windowId, { populate: true }, (window) => resolve(window))
|
||||
);
|
||||
return await BrowserApi.getWindowById(windowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active browser window.
|
||||
*/
|
||||
static async getCurrentWindow(): Promise<chrome.windows.Window> {
|
||||
return new Promise((resolve) => chrome.windows.getCurrent({ populate: true }, resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the window with the given id.
|
||||
*
|
||||
* @param windowId - The id of the window to get.
|
||||
*/
|
||||
static async getWindowById(windowId: number): Promise<chrome.windows.Window> {
|
||||
return new Promise((resolve) => chrome.windows.get(windowId, { populate: true }, resolve));
|
||||
}
|
||||
|
||||
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
|
||||
@ -37,8 +56,39 @@ export class BrowserApi {
|
||||
);
|
||||
}
|
||||
|
||||
static async removeWindow(windowId: number) {
|
||||
await chrome.windows.remove(windowId);
|
||||
/**
|
||||
* Removes the window with the given id.
|
||||
*
|
||||
* @param windowId - The id of the window to remove.
|
||||
*/
|
||||
static async removeWindow(windowId: number): Promise<void> {
|
||||
return new Promise((resolve) => chrome.windows.remove(windowId, () => resolve()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the properties of the window with the given id.
|
||||
*
|
||||
* @param windowId - The id of the window to update.
|
||||
* @param options - The window properties to update.
|
||||
*/
|
||||
static async updateWindowProperties(
|
||||
windowId: number,
|
||||
options: chrome.windows.UpdateInfo
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) =>
|
||||
chrome.windows.update(windowId, options, () => {
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the window with the given id.
|
||||
*
|
||||
* @param windowId - The id of the window to focus.
|
||||
*/
|
||||
static async focusWindow(windowId: number) {
|
||||
await BrowserApi.updateWindowProperties(windowId, { focused: true });
|
||||
}
|
||||
|
||||
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
||||
@ -138,10 +188,6 @@ export class BrowserApi {
|
||||
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
|
||||
}
|
||||
|
||||
static async removeTab(tabId: number) {
|
||||
await chrome.tabs.remove(tabId);
|
||||
}
|
||||
|
||||
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
||||
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
||||
}
|
||||
@ -172,47 +218,6 @@ export class BrowserApi {
|
||||
);
|
||||
}
|
||||
|
||||
static async focusWindow(windowId: number) {
|
||||
await chrome.windows.update(windowId, { focused: true });
|
||||
}
|
||||
|
||||
static async openBitwardenExtensionTab(relativeUrl: string, active = true) {
|
||||
let url = relativeUrl;
|
||||
if (!relativeUrl.includes("uilocation=tab")) {
|
||||
const fullUrl = chrome.extension.getURL(relativeUrl);
|
||||
const parsedUrl = new URL(fullUrl);
|
||||
parsedUrl.searchParams.set("uilocation", "tab");
|
||||
url = parsedUrl.toString();
|
||||
}
|
||||
|
||||
const createdTab = await this.createNewTab(url, active);
|
||||
this.focusWindow(createdTab.windowId);
|
||||
}
|
||||
|
||||
static async closeBitwardenExtensionTab() {
|
||||
const tabs = await BrowserApi.tabsQuery({
|
||||
active: true,
|
||||
title: "Bitwarden",
|
||||
windowType: "normal",
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabToClose = tabs[tabs.length - 1];
|
||||
BrowserApi.removeTab(tabToClose.id);
|
||||
}
|
||||
|
||||
static createNewWindow(
|
||||
url: string,
|
||||
focused = true,
|
||||
type: chrome.windows.createTypeEnum = "normal"
|
||||
) {
|
||||
chrome.windows.create({ url, focused, type });
|
||||
}
|
||||
|
||||
// Keep track of all the events registered in a Safari popup so we can remove
|
||||
// them when the popup gets unloaded, otherwise we cause a memory leak
|
||||
private static registeredMessageListeners: any[] = [];
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
interface BrowserPopoutWindowService {
|
||||
openUnlockPrompt(senderWindowId: number): Promise<void>;
|
||||
closeUnlockPrompt(): Promise<void>;
|
||||
openPasswordRepromptPrompt(
|
||||
senderWindowId: number,
|
||||
promptData: {
|
||||
action: string;
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
}
|
||||
): Promise<void>;
|
||||
openCipherCreation(
|
||||
senderWindowId: number,
|
||||
promptData: {
|
||||
cipherType?: CipherType;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
): Promise<void>;
|
||||
openCipherEdit(
|
||||
senderWindowId: number,
|
||||
promptData: {
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
): Promise<void>;
|
||||
closePasswordRepromptPrompt(): Promise<void>;
|
||||
openFido2Popout(
|
||||
senderWindow: chrome.tabs.Tab,
|
||||
promptData: {
|
||||
sessionId: string;
|
||||
senderTabId: number;
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
): Promise<number>;
|
||||
closeFido2Popout(): Promise<void>;
|
||||
}
|
||||
|
||||
export { BrowserPopoutWindowService };
|
@ -0,0 +1,6 @@
|
||||
type ScrollOptions = {
|
||||
delay: number;
|
||||
containerSelector: string;
|
||||
};
|
||||
|
||||
export { ScrollOptions };
|
@ -1,174 +0,0 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import { BrowserPopoutWindowService as BrowserPopupWindowServiceInterface } from "./abstractions/browser-popout-window.service";
|
||||
|
||||
class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
|
||||
private singleActionPopoutTabIds: Record<string, number> = {};
|
||||
private defaultPopoutWindowOptions: chrome.windows.CreateData = {
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
};
|
||||
|
||||
async openUnlockPrompt(senderWindowId: number) {
|
||||
await this.openSingleActionPopout(
|
||||
senderWindowId,
|
||||
"popup/index.html?uilocation=popout",
|
||||
"unlockPrompt"
|
||||
);
|
||||
}
|
||||
|
||||
async closeUnlockPrompt() {
|
||||
await this.closeSingleActionPopout("unlockPrompt");
|
||||
}
|
||||
|
||||
async openPasswordRepromptPrompt(
|
||||
senderWindowId: number,
|
||||
{
|
||||
cipherId,
|
||||
senderTabId,
|
||||
action,
|
||||
}: {
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
action: string;
|
||||
}
|
||||
) {
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/view-cipher" +
|
||||
"?uilocation=popout" +
|
||||
`&cipherId=${cipherId}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&action=${action}`;
|
||||
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt");
|
||||
}
|
||||
|
||||
async openCipherCreation(
|
||||
senderWindowId: number,
|
||||
{
|
||||
cipherType = CipherType.Login,
|
||||
senderTabId,
|
||||
senderTabURI,
|
||||
}: {
|
||||
cipherType?: CipherType;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
) {
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/edit-cipher" +
|
||||
"?uilocation=popout" +
|
||||
`&type=${cipherType}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&uri=${senderTabURI}`;
|
||||
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherCreation");
|
||||
}
|
||||
|
||||
async openCipherEdit(
|
||||
senderWindowId: number,
|
||||
{
|
||||
cipherId,
|
||||
senderTabId,
|
||||
senderTabURI,
|
||||
}: {
|
||||
cipherId: string;
|
||||
senderTabId: number;
|
||||
senderTabURI: string;
|
||||
}
|
||||
) {
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/edit-cipher" +
|
||||
"?uilocation=popout" +
|
||||
`&cipherId=${cipherId}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&uri=${senderTabURI}`;
|
||||
|
||||
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "cipherEdit");
|
||||
}
|
||||
|
||||
async closePasswordRepromptPrompt() {
|
||||
await this.closeSingleActionPopout("passwordReprompt");
|
||||
}
|
||||
|
||||
async openFido2Popout(
|
||||
senderWindow: chrome.tabs.Tab,
|
||||
{
|
||||
sessionId,
|
||||
senderTabId,
|
||||
fallbackSupported,
|
||||
}: {
|
||||
sessionId: string;
|
||||
senderTabId: number;
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
): Promise<number> {
|
||||
await this.closeFido2Popout();
|
||||
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/fido2" +
|
||||
"?uilocation=popout" +
|
||||
`&sessionId=${sessionId}` +
|
||||
`&fallbackSupported=${fallbackSupported}` +
|
||||
`&senderTabId=${senderTabId}` +
|
||||
`&senderUrl=${encodeURIComponent(senderWindow.url)}`;
|
||||
|
||||
return await this.openSingleActionPopout(
|
||||
senderWindow.windowId,
|
||||
promptWindowPath,
|
||||
"fido2Popout",
|
||||
{
|
||||
height: 450,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async closeFido2Popout(): Promise<void> {
|
||||
await this.closeSingleActionPopout("fido2Popout");
|
||||
}
|
||||
|
||||
private async openSingleActionPopout(
|
||||
senderWindowId: number,
|
||||
popupWindowURL: string,
|
||||
singleActionPopoutKey: string,
|
||||
options: chrome.windows.CreateData = {}
|
||||
): Promise<number> {
|
||||
const senderWindow = senderWindowId && (await BrowserApi.getWindow(senderWindowId));
|
||||
const url = chrome.extension.getURL(popupWindowURL);
|
||||
const offsetRight = 15;
|
||||
const offsetTop = 90;
|
||||
/// Use overrides in `options` if provided, otherwise use default
|
||||
const popupWidth = options?.width || this.defaultPopoutWindowOptions.width;
|
||||
const windowOptions = senderWindow
|
||||
? {
|
||||
...this.defaultPopoutWindowOptions,
|
||||
left: senderWindow.left + senderWindow.width - popupWidth - offsetRight,
|
||||
top: senderWindow.top + offsetTop,
|
||||
...options,
|
||||
url,
|
||||
}
|
||||
: { ...this.defaultPopoutWindowOptions, url, ...options };
|
||||
|
||||
const popupWindow = await BrowserApi.createWindow(windowOptions);
|
||||
|
||||
await this.closeSingleActionPopout(singleActionPopoutKey);
|
||||
this.singleActionPopoutTabIds[singleActionPopoutKey] = popupWindow?.tabs[0].id;
|
||||
|
||||
return popupWindow.id;
|
||||
}
|
||||
|
||||
private async closeSingleActionPopout(popoutKey: string) {
|
||||
const tabId = this.singleActionPopoutTabIds[popoutKey];
|
||||
|
||||
if (tabId) {
|
||||
await BrowserApi.removeTab(tabId);
|
||||
}
|
||||
this.singleActionPopoutTabIds[popoutKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowserPopoutWindowService;
|
448
apps/browser/src/platform/popup/browser-popup-utils.spec.ts
Normal file
448
apps/browser/src/platform/popup/browser-popup-utils.spec.ts
Normal file
@ -0,0 +1,448 @@
|
||||
import { createChromeTabMock } from "../../autofill/jest/autofill-mocks";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import BrowserPopupUtils from "./browser-popup-utils";
|
||||
|
||||
describe("BrowserPopupUtils", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("inSidebar", () => {
|
||||
it("should return true if the window contains the sidebar query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=sidebar" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inSidebar(win)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the window does not contain the sidebar query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=popout" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inSidebar(win)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inPopout", () => {
|
||||
it("should return true if the window contains the popout query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=popout" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inPopout(win)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the window does not contain the popout query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=sidebar" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inPopout(win)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inSingleActionPopout", () => {
|
||||
it("should return true if the window contains the singleActionPopout query param", () => {
|
||||
const win = {
|
||||
location: { href: "https://jest-testing.com?singleActionPopout=123" },
|
||||
} as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inSingleActionPopout(win, "123")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the window does not contain the singleActionPopout query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inSingleActionPopout(win, "123")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inPopup", () => {
|
||||
it("should return true if the window does not contain the popup query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inPopup(win)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if the window contains the popup query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=popup" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inPopup(win)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the window does not contain the popup query param", () => {
|
||||
const win = { location: { href: "https://jest-testing.com?uilocation=sidebar" } } as Window;
|
||||
|
||||
expect(BrowserPopupUtils.inPopup(win)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContentScrollY", () => {
|
||||
it("should return the scroll position of the popup", () => {
|
||||
const win = {
|
||||
document: { getElementsByTagName: () => [{ scrollTop: 100 }] },
|
||||
} as unknown as Window;
|
||||
|
||||
expect(BrowserPopupUtils.getContentScrollY(win)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setContentScrollY", () => {
|
||||
it("should set the scroll position of the popup", async () => {
|
||||
window.document.body.innerHTML = `
|
||||
<main>
|
||||
<div></div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
await BrowserPopupUtils.setContentScrollY(window, 200);
|
||||
|
||||
expect(window.document.getElementsByTagName("main")[0].scrollTop).toBe(200);
|
||||
});
|
||||
|
||||
it("should not set the scroll position of the popup if the scrollY is null", async () => {
|
||||
window.document.body.innerHTML = `
|
||||
<main>
|
||||
<div></div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
await BrowserPopupUtils.setContentScrollY(window, null);
|
||||
|
||||
expect(window.document.getElementsByTagName("main")[0].scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it("will set the scroll position of the popup after the provided delay", async () => {
|
||||
jest.useRealTimers();
|
||||
window.document.body.innerHTML = `
|
||||
<div class="scrolling-container">
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await BrowserPopupUtils.setContentScrollY(window, 300, {
|
||||
delay: 200,
|
||||
containerSelector: ".scrolling-container",
|
||||
});
|
||||
|
||||
expect(window.document.querySelector(".scrolling-container").scrollTop).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe("backgroundInitializationRequired", () => {
|
||||
it("return true if the background page is a null value", () => {
|
||||
jest.spyOn(BrowserApi, "getBackgroundPage").mockReturnValue(null);
|
||||
|
||||
expect(BrowserPopupUtils.backgroundInitializationRequired()).toBe(true);
|
||||
});
|
||||
|
||||
it("return false if the background page is not a null value", () => {
|
||||
jest.spyOn(BrowserApi, "getBackgroundPage").mockReturnValue({});
|
||||
|
||||
expect(BrowserPopupUtils.backgroundInitializationRequired()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inPrivateMode", () => {
|
||||
it("returns false if the background requires initialization", () => {
|
||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(false);
|
||||
|
||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if the manifest version is for version 3", () => {
|
||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true);
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||
|
||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true if the background does not require initalization and the manifest version is version 2", () => {
|
||||
jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true);
|
||||
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2);
|
||||
|
||||
expect(BrowserPopupUtils.inPrivateMode()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openPopout", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({
|
||||
id: 1,
|
||||
left: 100,
|
||||
top: 100,
|
||||
focused: false,
|
||||
alwaysOnTop: false,
|
||||
incognito: false,
|
||||
width: 380,
|
||||
});
|
||||
jest.spyOn(BrowserApi, "createWindow").mockImplementation();
|
||||
});
|
||||
|
||||
it("creates a window with the default window options", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url);
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout`,
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces any existing `uilocation=` query params within the passed extension url path to state the the uilocaiton is a popup", async () => {
|
||||
const url = "popup/index.html#/tabs/vault?uilocation=sidebar";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url);
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/popup/index.html#/tabs/vault?uilocation=popout`,
|
||||
});
|
||||
});
|
||||
|
||||
it("appends the uilocation to the search params if an existing param is passed with the extension url path", async () => {
|
||||
const url = "popup/index.html#/tabs/vault?existingParam=123";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url);
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/${url}&uilocation=popout`,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a single action popout window", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url, { singleActionKey: "123" });
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=123`,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create a single action popout window if it is already open", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(true);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url, { singleActionKey: "123" });
|
||||
|
||||
expect(BrowserApi.createWindow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a window with the provided window options", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url, {
|
||||
windowOptions: {
|
||||
type: "popup",
|
||||
focused: false,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
});
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: false,
|
||||
width: 100,
|
||||
height: 100,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout`,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens a single action window if the forceCloseExistingWindows param is true", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(true);
|
||||
|
||||
await BrowserPopupUtils.openPopout(url, {
|
||||
singleActionKey: "123",
|
||||
forceCloseExistingWindows: true,
|
||||
});
|
||||
|
||||
expect(BrowserApi.createWindow).toHaveBeenCalledWith({
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
left: 85,
|
||||
top: 190,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=123`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openCurrentPagePopout", () => {
|
||||
it("opens a popout window for the current page", async () => {
|
||||
const win = { location: { href: "https://example.com#/tabs/current" } } as Window;
|
||||
jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation();
|
||||
jest.spyOn(BrowserApi, "closePopup").mockImplementation();
|
||||
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(false);
|
||||
|
||||
await BrowserPopupUtils.openCurrentPagePopout(win);
|
||||
|
||||
expect(BrowserPopupUtils.openPopout).toHaveBeenCalledWith("/#/tabs/vault");
|
||||
expect(BrowserApi.closePopup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens a popout window for the specified URL", async () => {
|
||||
const win = {} as Window;
|
||||
jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation();
|
||||
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(false);
|
||||
|
||||
await BrowserPopupUtils.openCurrentPagePopout(win, "https://example.com#/settings");
|
||||
|
||||
expect(BrowserPopupUtils.openPopout).toHaveBeenCalledWith("/#/settings");
|
||||
});
|
||||
|
||||
it("opens a popout window for the current page and closes the popup window", async () => {
|
||||
const win = { location: { href: "https://example.com/#/tabs/vault" } } as Window;
|
||||
jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation();
|
||||
jest.spyOn(BrowserApi, "closePopup").mockImplementation();
|
||||
jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true);
|
||||
|
||||
await BrowserPopupUtils.openCurrentPagePopout(win);
|
||||
|
||||
expect(BrowserPopupUtils.openPopout).toHaveBeenCalledWith("/#/tabs/vault");
|
||||
expect(BrowserApi.closePopup).toHaveBeenCalledWith(win);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeSingleActionPopout", () => {
|
||||
it("closes any existing single action popouts", async () => {
|
||||
const url = "popup/index.html";
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([
|
||||
createChromeTabMock({
|
||||
id: 10,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=123`,
|
||||
windowId: 11,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 20,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=123`,
|
||||
windowId: 21,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 30,
|
||||
url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=456`,
|
||||
windowId: 31,
|
||||
}),
|
||||
]);
|
||||
jest.spyOn(BrowserApi, "removeWindow").mockResolvedValueOnce();
|
||||
|
||||
await BrowserPopupUtils.closeSingleActionPopout("123");
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(BrowserApi.removeWindow).toHaveBeenNthCalledWith(1, 11);
|
||||
expect(BrowserApi.removeWindow).toHaveBeenNthCalledWith(2, 21);
|
||||
expect(BrowserApi.removeWindow).not.toHaveBeenCalledWith(31);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSingleActionPopoutOpen", () => {
|
||||
const windowOptions = {
|
||||
id: 1,
|
||||
left: 100,
|
||||
top: 100,
|
||||
focused: false,
|
||||
alwaysOnTop: false,
|
||||
incognito: false,
|
||||
width: 500,
|
||||
height: 800,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "updateWindowProperties").mockImplementation();
|
||||
jest.spyOn(BrowserApi, "removeWindow").mockImplementation();
|
||||
});
|
||||
|
||||
it("returns false if the popoutKey is not provided", async () => {
|
||||
await expect(BrowserPopupUtils["isSingleActionPopoutOpen"](undefined, {})).resolves.toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false if no popout windows are found", async () => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
BrowserPopupUtils["isSingleActionPopoutOpen"]("123", windowOptions)
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if no single action popout is found relating to the popoutKey", async () => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([
|
||||
createChromeTabMock({
|
||||
id: 10,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=123`,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 20,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=123`,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 30,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=456`,
|
||||
}),
|
||||
]);
|
||||
|
||||
await expect(
|
||||
BrowserPopupUtils["isSingleActionPopoutOpen"]("789", windowOptions)
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true if a single action popout is found relating to the popoutKey", async () => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([
|
||||
createChromeTabMock({
|
||||
id: 10,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=123`,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 20,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=123`,
|
||||
}),
|
||||
createChromeTabMock({
|
||||
id: 30,
|
||||
url: `chrome-extension://id/popup/index.html?uilocation=popout&singleActionPopout=456`,
|
||||
}),
|
||||
]);
|
||||
|
||||
await expect(
|
||||
BrowserPopupUtils["isSingleActionPopoutOpen"]("123", windowOptions)
|
||||
).resolves.toBe(true);
|
||||
expect(BrowserApi.updateWindowProperties).toHaveBeenCalledWith(2, {
|
||||
focused: true,
|
||||
width: 500,
|
||||
height: 800,
|
||||
top: 100,
|
||||
left: 100,
|
||||
});
|
||||
expect(BrowserApi.removeWindow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
275
apps/browser/src/platform/popup/browser-popup-utils.ts
Normal file
275
apps/browser/src/platform/popup/browser-popup-utils.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions";
|
||||
|
||||
class BrowserPopupUtils {
|
||||
/**
|
||||
* Identifies if the popup is within the sidebar.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
*/
|
||||
static inSidebar(win: Window): boolean {
|
||||
return BrowserPopupUtils.urlContainsSearchParams(win, "uilocation", "sidebar");
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the popup is within the popout.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
*/
|
||||
static inPopout(win: Window): boolean {
|
||||
return BrowserPopupUtils.urlContainsSearchParams(win, "uilocation", "popout");
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the popup is within the single action popout.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
* @param popoutKey - The single action popout key used to identify the popout.
|
||||
*/
|
||||
static inSingleActionPopout(win: Window, popoutKey: string): boolean {
|
||||
return BrowserPopupUtils.urlContainsSearchParams(win, "singleActionPopout", popoutKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the popup is within the popup.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
*/
|
||||
static inPopup(win: Window): boolean {
|
||||
return (
|
||||
win.location.href.indexOf("uilocation=") === -1 ||
|
||||
win.location.href.indexOf("uilocation=popup") > -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scroll position of the popup.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
* @param scrollingContainer - Element tag name of the scrolling container.
|
||||
*/
|
||||
static getContentScrollY(win: Window, scrollingContainer = "main"): number {
|
||||
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
||||
return content.scrollTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll position of the popup.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
* @param scrollYAmount - The amount to scroll the popup.
|
||||
* @param options - Allows for setting the delay in ms to wait before scrolling the popup and the scrolling container tag name.
|
||||
*/
|
||||
static async setContentScrollY(
|
||||
win: Window,
|
||||
scrollYAmount: number | undefined,
|
||||
options: ScrollOptions = {
|
||||
delay: 0,
|
||||
containerSelector: "main",
|
||||
}
|
||||
) {
|
||||
const { delay, containerSelector } = options;
|
||||
return new Promise<void>((resolve) =>
|
||||
win.setTimeout(() => {
|
||||
const container = win.document.querySelector(containerSelector);
|
||||
if (!isNaN(scrollYAmount) && container) {
|
||||
container.scrollTop = scrollYAmount;
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, delay)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the background page needs to be initialized.
|
||||
*/
|
||||
static backgroundInitializationRequired() {
|
||||
return BrowserApi.getBackgroundPage() === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the popup is loading in private mode.
|
||||
*/
|
||||
static inPrivateMode() {
|
||||
return BrowserPopupUtils.backgroundInitializationRequired() && BrowserApi.manifestVersion !== 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popout window of any extension page. If the popout window is already open, it will be focused.
|
||||
*
|
||||
* @param extensionUrlPath - A relative path to the extension page. Example: "popup/index.html#/tabs/vault"
|
||||
* @param options - Options for the popout window that overrides the default options.
|
||||
*/
|
||||
static async openPopout(
|
||||
extensionUrlPath: string,
|
||||
options: {
|
||||
senderWindowId?: number;
|
||||
singleActionKey?: string;
|
||||
forceCloseExistingWindows?: boolean;
|
||||
windowOptions?: Partial<chrome.windows.CreateData>;
|
||||
} = {}
|
||||
) {
|
||||
const { senderWindowId, singleActionKey, forceCloseExistingWindows, windowOptions } = options;
|
||||
const defaultPopoutWindowOptions: chrome.windows.CreateData = {
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
height: 630,
|
||||
};
|
||||
const offsetRight = 15;
|
||||
const offsetTop = 90;
|
||||
const popupWidth = defaultPopoutWindowOptions.width;
|
||||
const senderWindow = await BrowserApi.getWindow(senderWindowId);
|
||||
const popoutWindowOptions = {
|
||||
left: senderWindow.left + senderWindow.width - popupWidth - offsetRight,
|
||||
top: senderWindow.top + offsetTop,
|
||||
...defaultPopoutWindowOptions,
|
||||
...windowOptions,
|
||||
url: chrome.runtime.getURL(
|
||||
BrowserPopupUtils.buildPopoutUrlPath(extensionUrlPath, singleActionKey)
|
||||
),
|
||||
};
|
||||
|
||||
if (
|
||||
(await BrowserPopupUtils.isSingleActionPopoutOpen(
|
||||
singleActionKey,
|
||||
popoutWindowOptions,
|
||||
forceCloseExistingWindows
|
||||
)) &&
|
||||
!forceCloseExistingWindows
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await BrowserApi.createWindow(popoutWindowOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the single action popout window.
|
||||
*
|
||||
* @param popoutKey - The single action popout key used to identify the popout.
|
||||
* @param delayClose - The amount of time to wait before closing the popout. Defaults to 0.
|
||||
*/
|
||||
static async closeSingleActionPopout(popoutKey: string, delayClose = 0): Promise<void> {
|
||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
||||
const tabs = await BrowserApi.tabsQuery({ url: `${extensionUrl}*` });
|
||||
for (const tab of tabs) {
|
||||
if (!tab.url.includes(`singleActionPopout=${popoutKey}`)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setTimeout(() => BrowserApi.removeWindow(tab.windowId), delayClose);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popout window for the current page.
|
||||
* If the current page is set for the current tab, then the
|
||||
* popout window will be set for the vault items listing tab.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
* @param href - The href to open in the popout window.
|
||||
*/
|
||||
static async openCurrentPagePopout(win: Window, href: string = null) {
|
||||
const popoutUrl = href || win.location.href;
|
||||
const parsedUrl = new URL(popoutUrl);
|
||||
let hashRoute = parsedUrl.hash;
|
||||
if (hashRoute.startsWith("#/tabs/current")) {
|
||||
hashRoute = "#/tabs/vault";
|
||||
}
|
||||
|
||||
await BrowserPopupUtils.openPopout(`${parsedUrl.pathname}${hashRoute}`);
|
||||
|
||||
if (BrowserPopupUtils.inPopup(win)) {
|
||||
BrowserApi.closePopup(win);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if a single action window is open based on the passed popoutKey.
|
||||
* Will focus the existing window, and close any other windows that might exist
|
||||
* with the same popout key.
|
||||
*
|
||||
* @param popoutKey - The single action popout key used to identify the popout.
|
||||
* @param windowInfo - The window info to use to update the existing window.
|
||||
* @param forceCloseExistingWindows - Identifies if the existing windows should be closed.
|
||||
*/
|
||||
private static async isSingleActionPopoutOpen(
|
||||
popoutKey: string | undefined,
|
||||
windowInfo: chrome.windows.CreateData,
|
||||
forceCloseExistingWindows = false
|
||||
) {
|
||||
if (!popoutKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
||||
const popoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter((tab) =>
|
||||
tab.url.includes(`singleActionPopout=${popoutKey}`)
|
||||
);
|
||||
if (popoutTabs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!forceCloseExistingWindows) {
|
||||
// Update first, remove it from list
|
||||
const tab = popoutTabs.shift();
|
||||
await BrowserApi.updateWindowProperties(tab.windowId, {
|
||||
focused: true,
|
||||
width: windowInfo.width,
|
||||
height: windowInfo.height,
|
||||
top: windowInfo.top,
|
||||
left: windowInfo.left,
|
||||
});
|
||||
}
|
||||
|
||||
popoutTabs.forEach((tab) => BrowserApi.removeWindow(tab.windowId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies if the url contains the specified search param and value.
|
||||
*
|
||||
* @param win - The passed window object.
|
||||
* @param searchParam - The search param to identify.
|
||||
* @param searchValue - The search value to identify.
|
||||
*/
|
||||
private static urlContainsSearchParams(
|
||||
win: Window,
|
||||
searchParam: string,
|
||||
searchValue: string
|
||||
): boolean {
|
||||
return win.location.href.indexOf(`${searchParam}=${searchValue}`) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the popout url path. Ensures that the uilocation param is set to
|
||||
* `popout` and that the singleActionPopout param is set to the passed singleActionKey.
|
||||
*
|
||||
* @param extensionUrlPath - A relative path to the extension page. Example: "popup/index.html#/tabs/vault"
|
||||
* @param singleActionKey - The single action popout key used to identify the popout.
|
||||
*/
|
||||
private static buildPopoutUrlPath(extensionUrlPath: string, singleActionKey: string) {
|
||||
let formattedExtensionUrlPath = extensionUrlPath;
|
||||
if (formattedExtensionUrlPath.includes("uilocation=")) {
|
||||
formattedExtensionUrlPath = formattedExtensionUrlPath.replace(
|
||||
/uilocation=[^&]*/g,
|
||||
"uilocation=popout"
|
||||
);
|
||||
} else {
|
||||
formattedExtensionUrlPath +=
|
||||
(formattedExtensionUrlPath.includes("?") ? "&" : "?") + "uilocation=popout";
|
||||
}
|
||||
|
||||
if (singleActionKey) {
|
||||
formattedExtensionUrlPath += `&singleActionPopout=${singleActionKey}`;
|
||||
}
|
||||
|
||||
return formattedExtensionUrlPath;
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowserPopupUtils;
|
@ -2,7 +2,7 @@ import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { PopupUtilsService } from "../services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-pop-out",
|
||||
@ -11,16 +11,13 @@ import { PopupUtilsService } from "../services/popup-utils.service";
|
||||
export class PopOutComponent implements OnInit {
|
||||
@Input() show = true;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private popupUtilsService: PopupUtilsService
|
||||
) {}
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.show) {
|
||||
if (
|
||||
(this.popupUtilsService.inSidebar(window) && this.platformUtilsService.isFirefox()) ||
|
||||
this.popupUtilsService.inPopout(window)
|
||||
(BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) ||
|
||||
BrowserPopupUtils.inPopout(window)
|
||||
) {
|
||||
this.show = false;
|
||||
}
|
||||
@ -28,6 +25,6 @@ export class PopOutComponent implements OnInit {
|
||||
}
|
||||
|
||||
expand() {
|
||||
this.popupUtilsService.popOut(window);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { PopupUtilsService } from "../services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-private-mode-warning",
|
||||
@ -9,9 +9,7 @@ import { PopupUtilsService } from "../services/popup-utils.service";
|
||||
export class PrivateModeWarningComponent implements OnInit {
|
||||
showWarning = false;
|
||||
|
||||
constructor(private popupUtilsService: PopupUtilsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.showWarning = this.popupUtilsService.inPrivateMode();
|
||||
this.showWarning = BrowserPopupUtils.inPrivateMode();
|
||||
}
|
||||
}
|
||||
|
@ -6,16 +6,14 @@ 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 BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||
|
||||
import { PopupUtilsService } from "./popup-utils.service";
|
||||
|
||||
@Injectable()
|
||||
export class InitService {
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
private stateService: StateServiceAbstraction,
|
||||
private logService: LogServiceAbstraction,
|
||||
private themingService: AbstractThemingService,
|
||||
@ -26,7 +24,7 @@ export class InitService {
|
||||
return async () => {
|
||||
await this.stateService.init();
|
||||
|
||||
if (!this.popupUtilsService.inPopup(window)) {
|
||||
if (!BrowserPopupUtils.inPopup(window)) {
|
||||
window.document.body.classList.add("body-full");
|
||||
} else if (window.screen.availHeight < 600) {
|
||||
window.document.body.classList.add("body-xs");
|
||||
@ -43,7 +41,7 @@ export class InitService {
|
||||
if (
|
||||
this.platformUtilsService.isChrome() &&
|
||||
navigator.platform.indexOf("Mac") > -1 &&
|
||||
this.popupUtilsService.inPopup(window) &&
|
||||
BrowserPopupUtils.inPopup(window) &&
|
||||
(window.screenLeft < 0 ||
|
||||
window.screenTop < 0 ||
|
||||
window.screenLeft > window.screen.width ||
|
||||
|
@ -0,0 +1,39 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { fromEvent, Subscription } from "rxjs";
|
||||
|
||||
@Injectable()
|
||||
export class PopupCloseWarningService {
|
||||
private unloadSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Enables a pop-up warning before the user exits the window/tab, or navigates away.
|
||||
* This warns the user that they may lose unsaved data if they leave the page.
|
||||
* (Note: navigating within the Angular app will not trigger it because it's an SPA.)
|
||||
* Make sure you call `PopupCloseWarningService.disable` when it is no longer relevant.
|
||||
*/
|
||||
enable() {
|
||||
this.disable();
|
||||
|
||||
this.unloadSubscription = fromEvent(window, "beforeunload").subscribe(
|
||||
(e: BeforeUnloadEvent) => {
|
||||
// Recommended method but not widely supported
|
||||
e.preventDefault();
|
||||
|
||||
// Modern browsers do not display this message, it just needs to be a non-nullish value
|
||||
// Exact wording is determined by the browser
|
||||
const confirmationMessage = "";
|
||||
|
||||
// Older methods with better support
|
||||
e.returnValue = confirmationMessage;
|
||||
return confirmationMessage;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the warning enabled by PopupCloseWarningService.enable.
|
||||
*/
|
||||
disable() {
|
||||
this.unloadSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { fromEvent, Subscription } from "rxjs";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
export type Popout =
|
||||
| {
|
||||
type: "window";
|
||||
window: chrome.windows.Window;
|
||||
}
|
||||
| {
|
||||
type: "tab";
|
||||
tab: chrome.tabs.Tab;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PopupUtilsService {
|
||||
private unloadSubscription: Subscription;
|
||||
|
||||
constructor(private privateMode: boolean = false) {}
|
||||
|
||||
inSidebar(win: Window): boolean {
|
||||
return win.location.search !== "" && win.location.search.indexOf("uilocation=sidebar") > -1;
|
||||
}
|
||||
|
||||
inTab(win: Window): boolean {
|
||||
return win.location.search !== "" && win.location.search.indexOf("uilocation=tab") > -1;
|
||||
}
|
||||
|
||||
inPopout(win: Window): boolean {
|
||||
return win.location.search !== "" && win.location.search.indexOf("uilocation=popout") > -1;
|
||||
}
|
||||
|
||||
inPopup(win: Window): boolean {
|
||||
return (
|
||||
win.location.search === "" ||
|
||||
win.location.search.indexOf("uilocation=") === -1 ||
|
||||
win.location.search.indexOf("uilocation=popup") > -1
|
||||
);
|
||||
}
|
||||
|
||||
inPrivateMode(): boolean {
|
||||
return this.privateMode;
|
||||
}
|
||||
|
||||
getContentScrollY(win: Window, scrollingContainer = "main"): number {
|
||||
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
||||
return content.scrollTop;
|
||||
}
|
||||
|
||||
setContentScrollY(win: Window, scrollY: number, scrollingContainer = "main"): void {
|
||||
if (scrollY != null) {
|
||||
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
||||
content.scrollTop = scrollY;
|
||||
}
|
||||
}
|
||||
|
||||
async popOut(
|
||||
win: Window,
|
||||
href: string = null,
|
||||
options: { center?: boolean } = {}
|
||||
): Promise<Popout> {
|
||||
if (href === null) {
|
||||
href = win.location.href;
|
||||
}
|
||||
|
||||
if (typeof chrome !== "undefined" && chrome?.windows?.create != null) {
|
||||
if (href.indexOf("?uilocation=") > -1) {
|
||||
href = href
|
||||
.replace("uilocation=popup", "uilocation=popout")
|
||||
.replace("uilocation=tab", "uilocation=popout")
|
||||
.replace("uilocation=sidebar", "uilocation=popout");
|
||||
} else {
|
||||
const hrefParts = href.split("#");
|
||||
href =
|
||||
hrefParts[0] + "?uilocation=popout" + (hrefParts.length > 0 ? "#" + hrefParts[1] : "");
|
||||
}
|
||||
|
||||
const bodyRect = document.querySelector("body").getBoundingClientRect();
|
||||
const width = Math.round(bodyRect.width ? bodyRect.width + 60 : 375);
|
||||
const height = Math.round(bodyRect.height || 600);
|
||||
const top = options.center ? Math.round((screen.height - height) / 2) : undefined;
|
||||
const left = options.center ? Math.round((screen.width - width) / 2) : undefined;
|
||||
const window = await BrowserApi.createWindow({
|
||||
url: href,
|
||||
type: "popup",
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
|
||||
if (win && this.inPopup(win)) {
|
||||
BrowserApi.closePopup(win);
|
||||
}
|
||||
|
||||
return { type: "window", window };
|
||||
} else if (chrome?.tabs?.create != null) {
|
||||
href = href
|
||||
.replace("uilocation=popup", "uilocation=tab")
|
||||
.replace("uilocation=popout", "uilocation=tab")
|
||||
.replace("uilocation=sidebar", "uilocation=tab");
|
||||
|
||||
const tab = await BrowserApi.createNewTab(href);
|
||||
return { type: "tab", tab };
|
||||
} else {
|
||||
throw new Error("Cannot open tab or window");
|
||||
}
|
||||
}
|
||||
|
||||
closePopOut(popout: Popout): Promise<void> {
|
||||
switch (popout.type) {
|
||||
case "window":
|
||||
return BrowserApi.removeWindow(popout.window.id);
|
||||
case "tab":
|
||||
return BrowserApi.removeTab(popout.tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a pop-up warning before the user exits the window/tab, or navigates away.
|
||||
* This warns the user that they may lose unsaved data if they leave the page.
|
||||
* (Note: navigating within the Angular app will not trigger it because it's an SPA.)
|
||||
* Make sure you call `disableTabCloseWarning` when it is no longer relevant.
|
||||
*/
|
||||
enableCloseTabWarning() {
|
||||
this.disableCloseTabWarning();
|
||||
|
||||
this.unloadSubscription = fromEvent(window, "beforeunload").subscribe(
|
||||
(e: BeforeUnloadEvent) => {
|
||||
// Recommended method but not widely supported
|
||||
e.preventDefault();
|
||||
|
||||
// Modern browsers do not display this message, it just needs to be a non-nullish value
|
||||
// Exact wording is determined by the browser
|
||||
const confirmationMessage = "";
|
||||
|
||||
// Older methods with better support
|
||||
e.returnValue = confirmationMessage;
|
||||
return confirmationMessage;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the warning enabled by enableCloseTabWarning.
|
||||
*/
|
||||
disableCloseTabWarning() {
|
||||
this.unloadSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
@ -94,6 +94,7 @@ import { AutofillService } from "../../autofill/services/abstractions/autofill.s
|
||||
import MainBackground from "../../background/main.background";
|
||||
import { Account } from "../../models/account";
|
||||
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";
|
||||
import { BrowserConfigService } from "../../platform/services/browser-config.service";
|
||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
||||
@ -110,11 +111,11 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
||||
|
||||
import { DebounceNavigationService } from "./debounceNavigationService";
|
||||
import { InitService } from "./init.service";
|
||||
import { PopupCloseWarningService } from "./popup-close-warning.service";
|
||||
import { PopupSearchService } from "./popup-search.service";
|
||||
import { PopupUtilsService } from "./popup-utils.service";
|
||||
|
||||
const needsBackgroundInit = BrowserApi.getBackgroundPage() == null;
|
||||
const isPrivateMode = needsBackgroundInit && BrowserApi.manifestVersion !== 3;
|
||||
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
|
||||
const isPrivateMode = BrowserPopupUtils.inPrivateMode();
|
||||
const mainBackground: MainBackground = needsBackgroundInit
|
||||
? createLocalBgService()
|
||||
: BrowserApi.getBackgroundPage().bitwardenMain;
|
||||
@ -138,6 +139,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||
InitService,
|
||||
DebounceNavigationService,
|
||||
DialogService,
|
||||
PopupCloseWarningService,
|
||||
{
|
||||
provide: LOCALE_ID,
|
||||
useFactory: () => getBgService<I18nServiceAbstraction>("i18nService")().translationLocale,
|
||||
@ -150,7 +152,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||
multi: true,
|
||||
},
|
||||
{ provide: BaseUnauthGuardService, useClass: UnauthGuardService },
|
||||
{ provide: PopupUtilsService, useFactory: () => new PopupUtilsService(isPrivateMode) },
|
||||
{
|
||||
provide: MessagingService,
|
||||
useFactory: () => {
|
||||
@ -523,13 +524,10 @@ function getBgService<T>(service: keyof MainBackground) {
|
||||
},
|
||||
{
|
||||
provide: FilePopoutUtilsService,
|
||||
useFactory: (
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
popupUtilsService: PopupUtilsService
|
||||
) => {
|
||||
return new FilePopoutUtilsService(platformUtilsService, popupUtilsService);
|
||||
useFactory: (platformUtilsService: PlatformUtilsService) => {
|
||||
return new FilePopoutUtilsService(platformUtilsService);
|
||||
},
|
||||
deps: [PlatformUtilsService, PopupUtilsService],
|
||||
deps: [PlatformUtilsService],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ import { DialogService } from "@bitwarden/components";
|
||||
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { PopupUtilsService } from "../services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||
|
||||
import { AboutComponent } from "./about.component";
|
||||
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
@ -95,7 +95,6 @@ export class SettingsComponent implements OnInit {
|
||||
private environmentService: EnvironmentService,
|
||||
private cryptoService: CryptoService,
|
||||
private stateService: StateService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
private modalService: ModalService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private dialogService: DialogService,
|
||||
@ -335,7 +334,7 @@ export class SettingsComponent implements OnInit {
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
|
||||
if (this.platformUtilsService.isFirefox() && this.popupUtilsService.inSidebar(window)) {
|
||||
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "nativeMessaginPermissionSidebarTitle" },
|
||||
content: { key: "nativeMessaginPermissionSidebarDesc" },
|
||||
@ -474,7 +473,7 @@ export class SettingsComponent implements OnInit {
|
||||
async import() {
|
||||
await this.router.navigate(["/import"]);
|
||||
if (await BrowserApi.isPopupOpen()) {
|
||||
this.popupUtilsService.popOut(window);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { PopupUtilsService } from "./services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-tabs",
|
||||
@ -9,9 +9,7 @@ import { PopupUtilsService } from "./services/popup-utils.service";
|
||||
export class TabsComponent implements OnInit {
|
||||
showCurrentTab = true;
|
||||
|
||||
constructor(private popupUtilsService: PopupUtilsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.showCurrentTab = !this.popupUtilsService.inPopout(window);
|
||||
this.showCurrentTab = !BrowserPopupUtils.inPopout(window);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Component, OnInit } from "@angular/core";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CalloutModule } from "@bitwarden/components";
|
||||
|
||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
||||
|
||||
@Component({
|
||||
@ -19,10 +19,7 @@ export class FilePopoutCalloutComponent implements OnInit {
|
||||
protected showSafariFileWarning: boolean;
|
||||
protected showChromiumFileWarning: boolean;
|
||||
|
||||
constructor(
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
private filePopoutUtilsService: FilePopoutUtilsService
|
||||
) {}
|
||||
constructor(private filePopoutUtilsService: FilePopoutUtilsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.showFilePopoutMessage = this.filePopoutUtilsService.showFilePopoutMessage(window);
|
||||
@ -32,6 +29,6 @@ export class FilePopoutCalloutComponent implements OnInit {
|
||||
}
|
||||
|
||||
popOutWindow() {
|
||||
this.popupUtilsService.popOut(window);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
||||
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
||||
|
||||
@Component({
|
||||
@ -44,7 +44,6 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
logService: LogService,
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
@ -68,14 +67,14 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
|
||||
popOutWindow() {
|
||||
this.popupUtilsService.popOut(window);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// File visibility
|
||||
this.showFileSelector =
|
||||
!this.editMode && !this.filePopoutUtilsService.showFilePopoutMessage(window);
|
||||
this.inPopout = this.popupUtilsService.inPopout(window);
|
||||
this.inPopout = BrowserPopupUtils.inPopout(window);
|
||||
this.isFirefox = this.platformUtilsService.isFirefox();
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
|
@ -17,8 +17,8 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
||||
|
||||
const ComponentId = "SendComponent";
|
||||
|
||||
@ -43,7 +43,6 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
||||
ngZone: NgZone,
|
||||
policyService: PolicyService,
|
||||
searchService: SearchService,
|
||||
private popupUtils: PopupUtilsService,
|
||||
private stateService: BrowserStateService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
@ -74,7 +73,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
||||
async ngOnInit() {
|
||||
// Determine Header details
|
||||
this.showLeftHeader = !(
|
||||
this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
);
|
||||
// Clear state of Send Type Component
|
||||
await this.stateService.setBrowserSendTypeComponentState(null);
|
||||
@ -97,7 +96,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0);
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||
}
|
||||
|
||||
// Load all sends if sync completed in background
|
||||
@ -172,7 +171,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
||||
|
||||
private async saveState() {
|
||||
this.state = Object.assign(new BrowserSendComponentState(), {
|
||||
scrollY: this.popupUtils.getContentScrollY(window),
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
sends: this.sends,
|
||||
typeCounts: this.typeCounts,
|
||||
|
@ -18,8 +18,8 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service.
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BrowserComponentState } from "../../../models/browserComponentState";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
||||
|
||||
const ComponentId = "SendTypeComponent";
|
||||
|
||||
@ -42,7 +42,6 @@ export class SendTypeComponent extends BaseSendComponent {
|
||||
ngZone: NgZone,
|
||||
policyService: PolicyService,
|
||||
searchService: SearchService,
|
||||
private popupUtils: PopupUtilsService,
|
||||
private stateService: BrowserStateService,
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
@ -102,7 +101,7 @@ export class SendTypeComponent extends BaseSendComponent {
|
||||
|
||||
// Restore state and remove reference
|
||||
if (this.applySavedState && this.state != null) {
|
||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0);
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||
}
|
||||
this.stateService.setBrowserSendTypeComponentState(null);
|
||||
});
|
||||
@ -163,7 +162,7 @@ export class SendTypeComponent extends BaseSendComponent {
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: this.popupUtils.getContentScrollY(window),
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.setBrowserSendTypeComponentState(this.state);
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
/**
|
||||
* Service for determining whether to display file popout callout messages.
|
||||
@ -12,10 +12,7 @@ export class FilePopoutUtilsService {
|
||||
/**
|
||||
* Creates an instance of FilePopoutUtilsService.
|
||||
*/
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private popupUtilsService: PopupUtilsService
|
||||
) {}
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
/**
|
||||
* Determines whether to show any file popout callout message in the current browser.
|
||||
@ -38,7 +35,7 @@ export class FilePopoutUtilsService {
|
||||
showFirefoxFileWarning(win: Window): boolean {
|
||||
return (
|
||||
this.platformUtilsService.isFirefox() &&
|
||||
!(this.popupUtilsService.inSidebar(win) || this.popupUtilsService.inPopout(win))
|
||||
!(BrowserPopupUtils.inSidebar(win) || BrowserPopupUtils.inPopout(win))
|
||||
);
|
||||
}
|
||||
|
||||
@ -48,7 +45,7 @@ export class FilePopoutUtilsService {
|
||||
* @returns True if the extension is not in a popout; otherwise, false.
|
||||
*/
|
||||
showSafariFileWarning(win: Window): boolean {
|
||||
return this.platformUtilsService.isSafari() && !this.popupUtilsService.inPopout(win);
|
||||
return this.platformUtilsService.isSafari() && !BrowserPopupUtils.inPopout(win);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +57,7 @@ export class FilePopoutUtilsService {
|
||||
return (
|
||||
(this.isLinux(win) || this.isUnsupportedMac(win)) &&
|
||||
!this.platformUtilsService.isFirefox() &&
|
||||
!(this.popupUtilsService.inSidebar(win) || this.popupUtilsService.inPopout(win))
|
||||
!(BrowserPopupUtils.inSidebar(win) || BrowserPopupUtils.inPopout(win))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
} from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserPopoutWindowService } from "../../platform/popup/abstractions/browser-popout-window.service";
|
||||
import { closeFido2Popout, openFido2Popout } from "../popup/utils/vault-popout-window";
|
||||
|
||||
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
|
||||
|
||||
@ -116,10 +116,7 @@ export type BrowserFido2Message = { sessionId: string } & (
|
||||
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
|
||||
*/
|
||||
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||
constructor(
|
||||
private browserPopoutWindowService: BrowserPopoutWindowService,
|
||||
private authService: AuthService
|
||||
) {}
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
async newSession(
|
||||
fallbackSupported: boolean,
|
||||
@ -127,7 +124,6 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
|
||||
abortController?: AbortController
|
||||
): Promise<Fido2UserInterfaceSession> {
|
||||
return await BrowserFido2UserInterfaceSession.create(
|
||||
this.browserPopoutWindowService,
|
||||
this.authService,
|
||||
fallbackSupported,
|
||||
tab,
|
||||
@ -138,14 +134,12 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
|
||||
|
||||
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
||||
static async create(
|
||||
browserPopoutWindowService: BrowserPopoutWindowService,
|
||||
authService: AuthService,
|
||||
fallbackSupported: boolean,
|
||||
tab: chrome.tabs.Tab,
|
||||
abortController?: AbortController
|
||||
): Promise<BrowserFido2UserInterfaceSession> {
|
||||
return new BrowserFido2UserInterfaceSession(
|
||||
browserPopoutWindowService,
|
||||
authService,
|
||||
fallbackSupported,
|
||||
tab,
|
||||
@ -183,7 +177,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private constructor(
|
||||
private readonly browserPopoutWindowService: BrowserPopoutWindowService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly fallbackSupported: boolean,
|
||||
private readonly tab: chrome.tabs.Tab,
|
||||
@ -304,7 +297,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.browserPopoutWindowService.closeFido2Popout();
|
||||
await closeFido2Popout(this.sessionId);
|
||||
this.closed = true;
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
@ -354,9 +347,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
)
|
||||
);
|
||||
|
||||
const popoutId = await this.browserPopoutWindowService.openFido2Popout(this.tab, {
|
||||
const popoutId = await openFido2Popout(this.tab, {
|
||||
sessionId: this.sessionId,
|
||||
senderTabId: this.tab.id,
|
||||
fallbackSupported: this.fallbackSupported,
|
||||
});
|
||||
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
BrowserFido2Message,
|
||||
BrowserFido2UserInterfaceSession,
|
||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||
import { VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
interface ViewData {
|
||||
message: BrowserFido2Message;
|
||||
@ -280,6 +281,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
uilocation: "popout",
|
||||
senderTabId: this.senderTabId,
|
||||
sessionId: this.sessionId,
|
||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -299,6 +301,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
||||
senderTabId: this.senderTabId,
|
||||
sessionId: this.sessionId,
|
||||
userVerification: data.userVerification,
|
||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -24,11 +24,13 @@ import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
||||
import {
|
||||
BrowserFido2UserInterfaceSession,
|
||||
fido2PopoutSessionData$,
|
||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||
import { VaultPopoutType, closeAddEditVaultItemPopout } from "../../utils/vault-popout-window";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
@ -40,9 +42,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
showAttachments = true;
|
||||
openAttachmentsInPopup: boolean;
|
||||
showAutoFillOnPageLoadOptions: boolean;
|
||||
senderTabId?: number;
|
||||
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
||||
inPopout = false;
|
||||
private singleActionKey: string;
|
||||
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
|
||||
@ -60,7 +60,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
private location: Location,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
private popupCloseWarningService: PopupCloseWarningService,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
@ -91,9 +91,6 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.senderTabId = parseInt(params?.senderTabId, 10) || undefined;
|
||||
this.uilocation = params?.uilocation;
|
||||
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
@ -119,18 +116,16 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
if (params.selectedVault) {
|
||||
this.organizationId = params.selectedVault;
|
||||
}
|
||||
if (params.singleActionKey) {
|
||||
this.singleActionKey = params.singleActionKey;
|
||||
}
|
||||
await this.load();
|
||||
|
||||
if (!this.editMode || this.cloneMode) {
|
||||
if (
|
||||
!this.popupUtilsService.inPopout(window) &&
|
||||
params.name &&
|
||||
(this.cipher.name == null || this.cipher.name === "")
|
||||
) {
|
||||
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
|
||||
this.cipher.name = params.name;
|
||||
}
|
||||
if (
|
||||
!this.popupUtilsService.inPopout(window) &&
|
||||
params.uri &&
|
||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||
) {
|
||||
@ -138,11 +133,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.openAttachmentsInPopup = this.popupUtilsService.inPopup(window);
|
||||
this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window);
|
||||
});
|
||||
|
||||
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
|
||||
|
||||
if (!this.editMode) {
|
||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||
this.currentUris =
|
||||
@ -153,8 +146,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
|
||||
this.setFocus();
|
||||
|
||||
if (this.popupUtilsService.inTab(window)) {
|
||||
this.popupUtilsService.enableCloseTabWarning();
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,12 +160,10 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
// Would be refactored after rework is done on the windows popout service
|
||||
|
||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
||||
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||
if (
|
||||
this.inPopout &&
|
||||
isFido2Session &&
|
||||
inFido2PopoutWindow &&
|
||||
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
||||
) {
|
||||
return false;
|
||||
@ -183,7 +174,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.inPopout && isFido2Session) {
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.disable();
|
||||
}
|
||||
|
||||
if (inFido2PopoutWindow) {
|
||||
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
|
||||
sessionId,
|
||||
this.cipher.id,
|
||||
@ -192,14 +187,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.popupUtilsService.inTab(window)) {
|
||||
this.popupUtilsService.disableCloseTabWarning();
|
||||
this.messagingService.send("closeTab", { delay: 1000 });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.senderTabId && this.inPopout) {
|
||||
setTimeout(() => this.close(), 1000);
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
await closeAddEditVaultItemPopout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -219,7 +208,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
|
||||
.toString();
|
||||
const currentBaseUrl = window.location.href.replace(this.router.url, "");
|
||||
this.popupUtilsService.popOut(window, currentBaseUrl + destinationUrl);
|
||||
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
|
||||
} else {
|
||||
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
@ -235,34 +224,21 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
async cancel() {
|
||||
super.cancel();
|
||||
|
||||
// Would be refactored after rework is done on the windows popout service
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
if (this.inPopout && sessionData.isFido2Session) {
|
||||
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
|
||||
this.popupCloseWarningService.disable();
|
||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.senderTabId && this.inPopout) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.popupUtilsService.inTab(window)) {
|
||||
this.messagingService.send("closeTab");
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
closeAddEditVaultItemPopout();
|
||||
return;
|
||||
}
|
||||
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
// Used for closing single-action views
|
||||
close() {
|
||||
BrowserApi.focusTab(this.senderTabId);
|
||||
window.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<boolean> {
|
||||
const confirmed = await super.generateUsername();
|
||||
if (confirmed) {
|
||||
@ -356,4 +332,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
this.i18nService.t("autofillOnPageLoadSetToDefault")
|
||||
);
|
||||
}
|
||||
|
||||
private inAddEditPopoutWindow() {
|
||||
return BrowserPopupUtils.inSingleActionPopout(
|
||||
window,
|
||||
this.singleActionKey || VaultPopoutType.addEditVaultItem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "CurrentTabComponent";
|
||||
@ -55,7 +55,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
private autofillService: AutofillService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
@ -72,7 +71,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.inSidebar = this.popupUtilsService.inSidebar(window);
|
||||
this.inSidebar = BrowserPopupUtils.inSidebar(window);
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
@ -185,7 +184,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
if (this.totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||
}
|
||||
if (this.popupUtilsService.inPopup(window)) {
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
if (!closePopupDelay) {
|
||||
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
|
||||
BrowserApi.closePopup(window);
|
||||
|
@ -19,8 +19,8 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultComponent";
|
||||
@ -80,7 +80,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
private popupUtils: PopupUtilsService,
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private searchService: SearchService,
|
||||
@ -94,7 +93,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.showLeftHeader = !(
|
||||
this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
);
|
||||
await this.browserStateService.setBrowserVaultItemsComponentState(null);
|
||||
|
||||
@ -136,7 +135,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state?.scrollY), 0);
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -265,7 +264,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (this.popupUtils.inPopup(window)) {
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
@ -376,7 +375,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async saveState() {
|
||||
this.state = Object.assign(new BrowserGroupingsComponentState(), {
|
||||
scrollY: this.popupUtils.getContentScrollY(window),
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
favoriteCiphers: this.favoriteCiphers,
|
||||
noFolderCiphers: this.noFolderCiphers,
|
||||
|
@ -13,7 +13,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
@ -21,8 +20,8 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserComponentState } from "../../../../models/browserComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultItemsComponent";
|
||||
@ -61,9 +60,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: BrowserStateService,
|
||||
private popupUtils: PopupUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private folderService: FolderService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
cipherService: CipherService,
|
||||
@ -155,11 +152,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
||||
}
|
||||
|
||||
if (this.applySavedState && this.state != null) {
|
||||
window.setTimeout(
|
||||
() =>
|
||||
this.popupUtils.setContentScrollY(window, this.state.scrollY, this.scrollingContainer),
|
||||
0
|
||||
);
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
|
||||
delay: 0,
|
||||
containerSelector: this.scrollingContainer,
|
||||
});
|
||||
}
|
||||
await this.stateService.setBrowserVaultItemsComponentState(null);
|
||||
});
|
||||
@ -219,7 +215,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (this.popupUtils.inPopup(window)) {
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
@ -288,7 +284,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer),
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.setBrowserVaultItemsComponentState(this.state);
|
||||
|
@ -28,7 +28,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import {
|
||||
BrowserFido2UserInterfaceSession,
|
||||
fido2PopoutSessionData$,
|
||||
@ -84,7 +84,6 @@ export class ViewComponent extends BaseViewComponent {
|
||||
eventCollectionService: EventCollectionService,
|
||||
private autofillService: AutofillService,
|
||||
private messagingService: MessagingService,
|
||||
private popupUtilsService: PopupUtilsService,
|
||||
apiService: ApiService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
@ -121,7 +120,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||
this.uilocation = value?.uilocation;
|
||||
});
|
||||
|
||||
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
|
||||
this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
|
149
apps/browser/src/vault/popup/utils/vault-popout-window.spec.ts
Normal file
149
apps/browser/src/vault/popup/utils/vault-popout-window.spec.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
import {
|
||||
closeAddEditVaultItemPopout,
|
||||
closeFido2Popout,
|
||||
openAddEditVaultItemPopout,
|
||||
openFido2Popout,
|
||||
openVaultItemPasswordRepromptPopout,
|
||||
VaultPopoutType,
|
||||
} from "./vault-popout-window";
|
||||
|
||||
describe("VaultPopoutWindow", () => {
|
||||
const openPopoutSpy = jest
|
||||
.spyOn(BrowserPopupUtils, "openPopout")
|
||||
.mockResolvedValue(mock<chrome.windows.Window>({ id: 10 }));
|
||||
const closeSingleActionPopoutSpy = jest
|
||||
.spyOn(BrowserPopupUtils, "closeSingleActionPopout")
|
||||
.mockImplementation();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("openVaultItemPasswordRepromptPopout", () => {
|
||||
it("opens a popout window that facilitates re-prompting for the password of a vault item", async () => {
|
||||
const senderTab = { windowId: 1 } as chrome.tabs.Tab;
|
||||
|
||||
await openVaultItemPasswordRepromptPopout(senderTab, {
|
||||
cipherId: "cipherId",
|
||||
action: "action",
|
||||
});
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/view-cipher?cipherId=cipherId&senderTabId=undefined&action=action",
|
||||
{
|
||||
singleActionKey: `${VaultPopoutType.vaultItemPasswordReprompt}_cipherId`,
|
||||
senderWindowId: 1,
|
||||
forceCloseExistingWindows: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openAddEditVaultItemPopout", () => {
|
||||
it("opens a popout window that facilitates adding a vault item", async () => {
|
||||
await openAddEditVaultItemPopout(
|
||||
mock<chrome.tabs.Tab>({ windowId: 1, url: "https://tacos.com" })
|
||||
);
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/edit-cipher?uilocation=popout&uri=https://tacos.com",
|
||||
{
|
||||
singleActionKey: VaultPopoutType.addEditVaultItem,
|
||||
senderWindowId: 1,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("opens a popout window that facilitates adding a specific type of vault item", () => {
|
||||
openAddEditVaultItemPopout(mock<chrome.tabs.Tab>({ windowId: 1, url: "https://tacos.com" }), {
|
||||
cipherType: CipherType.Identity,
|
||||
});
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
`popup/index.html#/edit-cipher?uilocation=popout&type=${CipherType.Identity}&uri=https://tacos.com`,
|
||||
{
|
||||
singleActionKey: `${VaultPopoutType.addEditVaultItem}_${CipherType.Identity}`,
|
||||
senderWindowId: 1,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("opens a popout window that facilitates editing a vault item", async () => {
|
||||
await openAddEditVaultItemPopout(
|
||||
mock<chrome.tabs.Tab>({ windowId: 1, url: "https://tacos.com" }),
|
||||
{
|
||||
cipherId: "cipherId",
|
||||
}
|
||||
);
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/edit-cipher?uilocation=popout&cipherId=cipherId&uri=https://tacos.com",
|
||||
{
|
||||
singleActionKey: `${VaultPopoutType.addEditVaultItem}_cipherId`,
|
||||
senderWindowId: 1,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeAddEditVaultItemPopout", () => {
|
||||
it("closes the add/edit vault item popout window", () => {
|
||||
closeAddEditVaultItemPopout();
|
||||
|
||||
expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(VaultPopoutType.addEditVaultItem, 0);
|
||||
});
|
||||
|
||||
it("closes the add/edit vault item popout window after a delay", () => {
|
||||
closeAddEditVaultItemPopout(1000);
|
||||
|
||||
expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(
|
||||
VaultPopoutType.addEditVaultItem,
|
||||
1000
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("openFido2Popout", () => {
|
||||
it("opens a popout window that facilitates FIDO2 authentication workflows", async () => {
|
||||
const senderTab = mock<chrome.tabs.Tab>({
|
||||
windowId: 1,
|
||||
url: "https://jest-testing.com",
|
||||
id: 2,
|
||||
});
|
||||
|
||||
const returnedWindowId = await openFido2Popout(senderTab, {
|
||||
sessionId: "sessionId",
|
||||
fallbackSupported: true,
|
||||
});
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/fido2?sessionId=sessionId&fallbackSupported=true&senderTabId=2&senderUrl=https%3A%2F%2Fjest-testing.com",
|
||||
{
|
||||
singleActionKey: `${VaultPopoutType.fido2Popout}_sessionId`,
|
||||
senderWindowId: 1,
|
||||
forceCloseExistingWindows: true,
|
||||
windowOptions: { height: 450 },
|
||||
}
|
||||
);
|
||||
expect(returnedWindowId).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeFido2Popout", () => {
|
||||
it("closes the fido2 popout window", () => {
|
||||
const sessionId = "sessionId";
|
||||
|
||||
closeFido2Popout(sessionId);
|
||||
|
||||
expect(closeSingleActionPopoutSpy).toHaveBeenCalledWith(
|
||||
`${VaultPopoutType.fido2Popout}_${sessionId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
129
apps/browser/src/vault/popup/utils/vault-popout-window.ts
Normal file
129
apps/browser/src/vault/popup/utils/vault-popout-window.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
const VaultPopoutType = {
|
||||
vaultItemPasswordReprompt: "vault_PasswordReprompt",
|
||||
addEditVaultItem: "vault_AddEditVaultItem",
|
||||
fido2Popout: "vault_Fido2Popout",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Opens a popout window that facilitates re-prompting for
|
||||
* the password of a vault item.
|
||||
*
|
||||
* @param senderTab - The tab that sent the request.
|
||||
* @param cipherOptions - The cipher id and action to perform.
|
||||
*/
|
||||
async function openVaultItemPasswordRepromptPopout(
|
||||
senderTab: chrome.tabs.Tab,
|
||||
cipherOptions: {
|
||||
cipherId: string;
|
||||
action: string;
|
||||
}
|
||||
) {
|
||||
const { cipherId, action } = cipherOptions;
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/view-cipher" +
|
||||
`?cipherId=${cipherId}` +
|
||||
`&senderTabId=${senderTab.id}` +
|
||||
`&action=${action}`;
|
||||
|
||||
await BrowserPopupUtils.openPopout(promptWindowPath, {
|
||||
singleActionKey: `${VaultPopoutType.vaultItemPasswordReprompt}_${cipherId}`,
|
||||
senderWindowId: senderTab.windowId,
|
||||
forceCloseExistingWindows: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popout window that facilitates adding or editing a vault item.
|
||||
*
|
||||
* @param senderTab - The window id of the sender.
|
||||
* @param cipherOptions - Options passed as query params to the popout.
|
||||
*/
|
||||
async function openAddEditVaultItemPopout(
|
||||
senderTab: chrome.tabs.Tab,
|
||||
cipherOptions: { cipherId?: string; cipherType?: CipherType } = {}
|
||||
) {
|
||||
const { cipherId, cipherType } = cipherOptions;
|
||||
const { url, windowId } = senderTab;
|
||||
|
||||
let singleActionKey = VaultPopoutType.addEditVaultItem;
|
||||
let addEditCipherUrl = "popup/index.html#/edit-cipher?uilocation=popout";
|
||||
if (cipherId && !cipherType) {
|
||||
singleActionKey += `_${cipherId}`;
|
||||
addEditCipherUrl += `&cipherId=${cipherId}`;
|
||||
}
|
||||
if (cipherType && !cipherId) {
|
||||
singleActionKey += `_${cipherType}`;
|
||||
addEditCipherUrl += `&type=${cipherType}`;
|
||||
}
|
||||
if (senderTab.url) {
|
||||
addEditCipherUrl += `&uri=${url}`;
|
||||
}
|
||||
|
||||
await BrowserPopupUtils.openPopout(addEditCipherUrl, {
|
||||
singleActionKey,
|
||||
senderWindowId: windowId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the add/edit vault item popout window.
|
||||
*
|
||||
* @param delayClose - The amount of time to wait before closing the popout. Defaults to 0.
|
||||
*/
|
||||
async function closeAddEditVaultItemPopout(delayClose = 0) {
|
||||
await BrowserPopupUtils.closeSingleActionPopout(VaultPopoutType.addEditVaultItem, delayClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popout window that facilitates FIDO2
|
||||
* authentication and passkey management.
|
||||
*
|
||||
* @param senderTab - The tab that sent the request.
|
||||
* @param options - Options passed as query params to the popout.
|
||||
*/
|
||||
async function openFido2Popout(
|
||||
senderTab: chrome.tabs.Tab,
|
||||
options: {
|
||||
sessionId: string;
|
||||
fallbackSupported: boolean;
|
||||
}
|
||||
): Promise<chrome.windows.Window["id"]> {
|
||||
const { sessionId, fallbackSupported } = options;
|
||||
const promptWindowPath =
|
||||
"popup/index.html#/fido2" +
|
||||
`?sessionId=${sessionId}` +
|
||||
`&fallbackSupported=${fallbackSupported}` +
|
||||
`&senderTabId=${senderTab.id}` +
|
||||
`&senderUrl=${encodeURIComponent(senderTab.url)}`;
|
||||
|
||||
const popoutWindow = await BrowserPopupUtils.openPopout(promptWindowPath, {
|
||||
singleActionKey: `${VaultPopoutType.fido2Popout}_${sessionId}`,
|
||||
senderWindowId: senderTab.windowId,
|
||||
forceCloseExistingWindows: true,
|
||||
windowOptions: { height: 450 },
|
||||
});
|
||||
|
||||
return popoutWindow.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the FIDO2 popout window associated with the passed session ID.
|
||||
*
|
||||
* @param sessionId - The session ID of the popout to close.
|
||||
*/
|
||||
async function closeFido2Popout(sessionId: string): Promise<void> {
|
||||
await BrowserPopupUtils.closeSingleActionPopout(`${VaultPopoutType.fido2Popout}_${sessionId}`);
|
||||
}
|
||||
|
||||
export {
|
||||
VaultPopoutType,
|
||||
openVaultItemPasswordRepromptPopout,
|
||||
openAddEditVaultItemPopout,
|
||||
closeAddEditVaultItemPopout,
|
||||
openFido2Popout,
|
||||
closeFido2Popout,
|
||||
};
|
@ -23,6 +23,7 @@ const runtime = {
|
||||
},
|
||||
sendMessage: jest.fn(),
|
||||
getManifest: jest.fn(),
|
||||
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
|
||||
};
|
||||
|
||||
const contextMenus = {
|
||||
@ -37,12 +38,21 @@ const i18n = {
|
||||
const tabs = {
|
||||
executeScript: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
query: jest.fn(),
|
||||
};
|
||||
|
||||
const scripting = {
|
||||
executeScript: jest.fn(),
|
||||
};
|
||||
|
||||
const windows = {
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getCurrent: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
// set chrome
|
||||
global.chrome = {
|
||||
i18n,
|
||||
@ -51,4 +61,5 @@ global.chrome = {
|
||||
contextMenus,
|
||||
tabs,
|
||||
scripting,
|
||||
windows,
|
||||
} as any;
|
||||
|
Loading…
Reference in New Issue
Block a user