mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00: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:
@@ -22,7 +22,9 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
|||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
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";
|
const BroadcasterSubscriptionId = "TwoFactorComponent";
|
||||||
|
|
||||||
@@ -31,8 +33,6 @@ const BroadcasterSubscriptionId = "TwoFactorComponent";
|
|||||||
templateUrl: "two-factor.component.html",
|
templateUrl: "two-factor.component.html",
|
||||||
})
|
})
|
||||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||||
showNewWindowMessage = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
router: Router,
|
router: Router,
|
||||||
@@ -42,7 +42,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
stateService: StateService,
|
stateService: StateService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
@@ -115,7 +114,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.selectedProviderType === TwoFactorProviderType.Email &&
|
this.selectedProviderType === TwoFactorProviderType.Email &&
|
||||||
this.popupUtilsService.inPopup(window)
|
BrowserPopupUtils.inPopup(window)
|
||||||
) {
|
) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "warning" },
|
title: { key: "warning" },
|
||||||
@@ -123,7 +122,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
|
|||||||
type: "warning",
|
type: "warning",
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
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
|
// 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)
|
// 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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
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 AddUnlockVaultQueueMessage from "../../background/models/add-unlock-vault-queue-message";
|
||||||
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
|
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
|
||||||
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
|
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
|
||||||
@@ -21,6 +22,7 @@ import LockedVaultPendingNotificationsItem from "../../background/models/lockedV
|
|||||||
import { NotificationQueueMessageType } from "../../background/models/notificationQueueMessageType";
|
import { NotificationQueueMessageType } from "../../background/models/notificationQueueMessageType";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
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";
|
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||||
|
|
||||||
export default class NotificationBackground {
|
export default class NotificationBackground {
|
||||||
@@ -96,7 +98,7 @@ export default class NotificationBackground {
|
|||||||
"addToLockedVaultPendingNotifications",
|
"addToLockedVaultPendingNotifications",
|
||||||
retryMessage
|
retryMessage
|
||||||
);
|
);
|
||||||
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
|
await openUnlockPopout(sender.tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.saveOrUpdateCredentials(sender.tab, msg.edit, msg.folder);
|
await this.saveOrUpdateCredentials(sender.tab, msg.edit, msg.folder);
|
||||||
@@ -118,9 +120,12 @@ export default class NotificationBackground {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "promptForLogin":
|
case "bgUnlockPopoutOpened":
|
||||||
await this.unlockVault(sender.tab);
|
await this.unlockVault(sender.tab);
|
||||||
break;
|
break;
|
||||||
|
case "bgReopenUnlockPopout":
|
||||||
|
await openUnlockPopout(sender.tab);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -447,9 +452,7 @@ export default class NotificationBackground {
|
|||||||
collectionIds: cipherView.collectionIds,
|
collectionIds: cipherView.collectionIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
await BrowserApi.tabSendMessageData(senderTab, "openAddEditCipher", {
|
await openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
|
||||||
cipherId: cipherView.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async folderExists(folderId: string) {
|
private async folderExists(folderId: string) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "../../auth/background/service-factories/auth-service.factory";
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory";
|
||||||
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-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 LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem";
|
||||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
@@ -29,6 +30,10 @@ import {
|
|||||||
cipherServiceFactory,
|
cipherServiceFactory,
|
||||||
CipherServiceInitOptions,
|
CipherServiceInitOptions,
|
||||||
} from "../../vault/background/service_factories/cipher-service.factory";
|
} 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 { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
|
||||||
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
import { copyToClipboard, GeneratePasswordToClipboardCommand } from "../clipboard";
|
||||||
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
import { AutofillTabCommand } from "../commands/autofill-tab-command";
|
||||||
@@ -187,7 +192,7 @@ export class ContextMenuClickedHandler {
|
|||||||
retryMessage
|
retryMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
await BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
await openUnlockPopout(tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,14 +240,12 @@ export class ContextMenuClickedHandler {
|
|||||||
const cipherType = this.getCipherCreationType(menuItemId);
|
const cipherType = this.getCipherCreationType(menuItemId);
|
||||||
|
|
||||||
if (cipherType) {
|
if (cipherType) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
await openAddEditVaultItemPopout(tab, { cipherType });
|
||||||
cipherType,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await openVaultItemPasswordRepromptPopout(tab, {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
// The action here is passed on to the single-use reprompt window and doesn't change based on cipher type
|
// The action here is passed on to the single-use reprompt window and doesn't change based on cipher type
|
||||||
action: AUTOFILL_ID,
|
action: AUTOFILL_ID,
|
||||||
@@ -255,9 +258,7 @@ export class ContextMenuClickedHandler {
|
|||||||
}
|
}
|
||||||
case COPY_USERNAME_ID:
|
case COPY_USERNAME_ID:
|
||||||
if (menuItemId === CREATE_LOGIN_ID) {
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||||
cipherType: CipherType.Login,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,16 +266,14 @@ export class ContextMenuClickedHandler {
|
|||||||
break;
|
break;
|
||||||
case COPY_PASSWORD_ID:
|
case COPY_PASSWORD_ID:
|
||||||
if (menuItemId === CREATE_LOGIN_ID) {
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||||
cipherType: CipherType.Login,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await openVaultItemPasswordRepromptPopout(tab, {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: info.parentMenuItemId,
|
action: COPY_PASSWORD_ID,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
this.copyToClipboard({ text: cipher.login.password, tab: tab });
|
||||||
@@ -284,16 +283,14 @@ export class ContextMenuClickedHandler {
|
|||||||
break;
|
break;
|
||||||
case COPY_VERIFICATIONCODE_ID:
|
case COPY_VERIFICATIONCODE_ID:
|
||||||
if (menuItemId === CREATE_LOGIN_ID) {
|
if (menuItemId === CREATE_LOGIN_ID) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "openAddEditCipher", {
|
await openAddEditVaultItemPopout(tab, { cipherType: CipherType.Login });
|
||||||
cipherType: CipherType.Login,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isPasswordRepromptRequired(cipher)) {
|
if (await this.isPasswordRepromptRequired(cipher)) {
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await openVaultItemPasswordRepromptPopout(tab, {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: info.parentMenuItemId,
|
action: COPY_VERIFICATIONCODE_ID,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyToClipboard({
|
this.copyToClipboard({
|
||||||
|
|||||||
@@ -28,12 +28,10 @@ window.addEventListener(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const forwardCommands = [
|
const forwardCommands = [
|
||||||
"promptForLogin",
|
"bgUnlockPopoutOpened",
|
||||||
"passwordReprompt",
|
|
||||||
"addToLockedVaultPendingNotifications",
|
"addToLockedVaultPendingNotifications",
|
||||||
"unlockCompleted",
|
"unlockCompleted",
|
||||||
"addedCipher",
|
"addedCipher",
|
||||||
"openAddEditCipher",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((event) => {
|
chrome.runtime.onMessage.addListener((event) => {
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ function handleTypeUnlock() {
|
|||||||
const unlockButton = document.getElementById("unlock-vault");
|
const unlockButton = document.getElementById("unlock-vault");
|
||||||
unlockButton.addEventListener("click", (e) => {
|
unlockButton.addEventListener("click", (e) => {
|
||||||
sendPlatformMessage({
|
sendPlatformMessage({
|
||||||
command: "bgReopenPromptForLogin",
|
command: "bgReopenUnlockPopout",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -752,13 +752,15 @@ describe("AutofillService", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(userVerificationService, "hasMasterPasswordAndMasterKeyHash")
|
.spyOn(userVerificationService, "hasMasterPasswordAndMasterKeyHash")
|
||||||
.mockResolvedValueOnce(true);
|
.mockResolvedValueOnce(true);
|
||||||
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
|
jest
|
||||||
|
.spyOn(autofillService as any, "openVaultItemPasswordRepromptPopout")
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
|
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
|
||||||
|
|
||||||
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
|
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
|
||||||
expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
|
expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
|
||||||
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(tab, "passwordReprompt", {
|
expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: "autofill",
|
action: "autofill",
|
||||||
});
|
});
|
||||||
@@ -4254,4 +4256,24 @@ describe("AutofillService", () => {
|
|||||||
expect(result).toBe(false);
|
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 { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
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 AutofillField from "../models/autofill-field";
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import AutofillScript from "../models/autofill-script";
|
import AutofillScript from "../models/autofill-script";
|
||||||
@@ -30,6 +31,10 @@ import {
|
|||||||
} from "./autofill-constants";
|
} from "./autofill-constants";
|
||||||
|
|
||||||
export default class AutofillService implements AutofillServiceInterface {
|
export default class AutofillService implements AutofillServiceInterface {
|
||||||
|
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
|
||||||
|
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
|
||||||
|
private currentlyOpeningPasswordRepromptPopout = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
@@ -272,13 +277,14 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
if (
|
if (
|
||||||
cipher.reprompt === CipherRepromptType.Password &&
|
cipher.reprompt === CipherRepromptType.Password &&
|
||||||
// If the master password has is not available, reprompt will error
|
// If the master password has is not available, reprompt will error
|
||||||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
|
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) &&
|
||||||
|
!this.isDebouncingPasswordRepromptPopout()
|
||||||
) {
|
) {
|
||||||
if (fromCommand) {
|
if (fromCommand) {
|
||||||
this.cipherService.updateLastUsedIndexForUrl(tab.url);
|
this.cipherService.updateLastUsedIndexForUrl(tab.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
|
await this.openVaultItemPasswordRepromptPopout(tab, {
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
action: "autofill",
|
action: "autofill",
|
||||||
});
|
});
|
||||||
@@ -1828,4 +1834,22 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
static forCustomFieldsOnly(field: AutofillField): boolean {
|
static forCustomFieldsOnly(field: AutofillField): boolean {
|
||||||
return field.tagName === "span";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
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 { BrowserApi } from "../platform/browser/browser-api";
|
||||||
|
|
||||||
import MainBackground from "./main.background";
|
import MainBackground from "./main.background";
|
||||||
@@ -87,7 +88,7 @@ export default class CommandsBackground {
|
|||||||
retryMessage
|
retryMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
BrowserApi.tabSendMessageData(tab, "promptForLogin");
|
await openUnlockPopout(tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ import { Account } from "../models/account";
|
|||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
import { flagEnabled } from "../platform/flags";
|
import { flagEnabled } from "../platform/flags";
|
||||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
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 { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserConfigService } from "../platform/services/browser-config.service";
|
import { BrowserConfigService } from "../platform/services/browser-config.service";
|
||||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.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 { BrowserStateService } from "../platform/services/browser-state.service";
|
||||||
import { KeyGenerationService } from "../platform/services/key-generation.service";
|
import { KeyGenerationService } from "../platform/services/key-generation.service";
|
||||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.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 { BrowserSendService } from "../services/browser-send.service";
|
||||||
import { BrowserSettingsService } from "../services/browser-settings.service";
|
import { BrowserSettingsService } from "../services/browser-settings.service";
|
||||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
||||||
@@ -225,8 +223,6 @@ export default class MainBackground {
|
|||||||
devicesService: DevicesServiceAbstraction;
|
devicesService: DevicesServiceAbstraction;
|
||||||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
||||||
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
|
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
|
||||||
popupUtilsService: PopupUtilsService;
|
|
||||||
browserPopoutWindowService: BrowserPopoutWindowService;
|
|
||||||
accountService: AccountServiceAbstraction;
|
accountService: AccountServiceAbstraction;
|
||||||
|
|
||||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||||
@@ -583,12 +579,7 @@ export default class MainBackground {
|
|||||||
this.messagingService
|
this.messagingService
|
||||||
);
|
);
|
||||||
|
|
||||||
this.browserPopoutWindowService = new BrowserPopoutWindowService();
|
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
||||||
|
|
||||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(
|
|
||||||
this.browserPopoutWindowService,
|
|
||||||
this.authService
|
|
||||||
);
|
|
||||||
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
|
this.fido2AuthenticatorService = new Fido2AuthenticatorService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.fido2UserInterfaceService,
|
this.fido2UserInterfaceService,
|
||||||
@@ -634,8 +625,7 @@ export default class MainBackground {
|
|||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService
|
||||||
this.browserPopoutWindowService
|
|
||||||
);
|
);
|
||||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se
|
|||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
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 { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
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 { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
||||||
@@ -37,8 +41,7 @@ export default class RuntimeBackground {
|
|||||||
private environmentService: BrowserEnvironmentService,
|
private environmentService: BrowserEnvironmentService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction
|
||||||
private browserPopoutWindowService: BrowserPopoutWindowService
|
|
||||||
) {
|
) {
|
||||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||||
@@ -82,8 +85,6 @@ export default class RuntimeBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
|
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
|
||||||
const cipherId = msg.data?.cipherId;
|
|
||||||
|
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case "loggedIn":
|
case "loggedIn":
|
||||||
case "unlocked": {
|
case "unlocked": {
|
||||||
@@ -91,7 +92,7 @@ export default class RuntimeBackground {
|
|||||||
|
|
||||||
if (this.lockedVaultPendingNotifications?.length > 0) {
|
if (this.lockedVaultPendingNotifications?.length > 0) {
|
||||||
item = this.lockedVaultPendingNotifications.pop();
|
item = this.lockedVaultPendingNotifications.pop();
|
||||||
await this.browserPopoutWindowService.closeUnlockPrompt();
|
await closeUnlockPopout();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.main.refreshBadge();
|
await this.main.refreshBadge();
|
||||||
@@ -129,49 +130,6 @@ export default class RuntimeBackground {
|
|||||||
case "openPopup":
|
case "openPopup":
|
||||||
await this.main.openPopup();
|
await this.main.openPopup();
|
||||||
break;
|
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":
|
case "triggerAutofillScriptInjection":
|
||||||
await this.autofillService.injectAutofillScripts(
|
await this.autofillService.injectAutofillScripts(
|
||||||
sender,
|
sender,
|
||||||
@@ -266,12 +224,7 @@ export default class RuntimeBackground {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
BrowserApi.createNewTab(
|
await openSsoAuthResultPopout(msg);
|
||||||
"popup/index.html?uilocation=popout#/sso?code=" +
|
|
||||||
encodeURIComponent(msg.code) +
|
|
||||||
"&state=" +
|
|
||||||
encodeURIComponent(msg.state)
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
this.logService.error("Unable to open sso popout tab");
|
this.logService.error("Unable to open sso popout tab");
|
||||||
}
|
}
|
||||||
@@ -285,10 +238,7 @@ export default class RuntimeBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params =
|
await openTwoFactorAuthPopout(msg);
|
||||||
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
|
|
||||||
`remember=${encodeURIComponent(msg.remember)}`;
|
|
||||||
BrowserApi.openBitwardenExtensionTab(`popup/index.html#/2fa;${params}`, false);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "reloadPopup":
|
case "reloadPopup":
|
||||||
|
|||||||
@@ -9,6 +9,89 @@ describe("BrowserApi", () => {
|
|||||||
jest.clearAllMocks();
|
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", () => {
|
describe("executeScriptInTab", () => {
|
||||||
it("calls to the extension api to execute a script within the give tabId", async () => {
|
it("calls to the extension api to execute a script within the give tabId", async () => {
|
||||||
const tabId = 1;
|
const tabId = 1;
|
||||||
|
|||||||
@@ -19,14 +19,33 @@ export class BrowserApi {
|
|||||||
return chrome.runtime.getManifest().manifest_version;
|
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) {
|
if (!windowId) {
|
||||||
return;
|
return BrowserApi.getCurrentWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) =>
|
return await BrowserApi.getWindowById(windowId);
|
||||||
chrome.windows.get(windowId, { populate: true }, (window) => resolve(window))
|
}
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
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 {
|
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
||||||
@@ -138,10 +188,6 @@ export class BrowserApi {
|
|||||||
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
|
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[]> {
|
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
|
||||||
return (await browser.windows.getAll()).filter((win) => win.incognito);
|
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
|
// 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
|
// them when the popup gets unloaded, otherwise we cause a memory leak
|
||||||
private static registeredMessageListeners: any[] = [];
|
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 { 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({
|
@Component({
|
||||||
selector: "app-pop-out",
|
selector: "app-pop-out",
|
||||||
@@ -11,16 +11,13 @@ import { PopupUtilsService } from "../services/popup-utils.service";
|
|||||||
export class PopOutComponent implements OnInit {
|
export class PopOutComponent implements OnInit {
|
||||||
@Input() show = true;
|
@Input() show = true;
|
||||||
|
|
||||||
constructor(
|
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private popupUtilsService: PopupUtilsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.show) {
|
if (this.show) {
|
||||||
if (
|
if (
|
||||||
(this.popupUtilsService.inSidebar(window) && this.platformUtilsService.isFirefox()) ||
|
(BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) ||
|
||||||
this.popupUtilsService.inPopout(window)
|
BrowserPopupUtils.inPopout(window)
|
||||||
) {
|
) {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
}
|
}
|
||||||
@@ -28,6 +25,6 @@ export class PopOutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expand() {
|
expand() {
|
||||||
this.popupUtilsService.popOut(window);
|
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
|
||||||
import { PopupUtilsService } from "../services/popup-utils.service";
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-private-mode-warning",
|
selector: "app-private-mode-warning",
|
||||||
@@ -9,9 +9,7 @@ import { PopupUtilsService } from "../services/popup-utils.service";
|
|||||||
export class PrivateModeWarningComponent implements OnInit {
|
export class PrivateModeWarningComponent implements OnInit {
|
||||||
showWarning = false;
|
showWarning = false;
|
||||||
|
|
||||||
constructor(private popupUtilsService: PopupUtilsService) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.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 { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||||
|
|
||||||
import { PopupUtilsService } from "./popup-utils.service";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InitService {
|
export class InitService {
|
||||||
constructor(
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
private stateService: StateServiceAbstraction,
|
private stateService: StateServiceAbstraction,
|
||||||
private logService: LogServiceAbstraction,
|
private logService: LogServiceAbstraction,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
@@ -26,7 +24,7 @@ export class InitService {
|
|||||||
return async () => {
|
return async () => {
|
||||||
await this.stateService.init();
|
await this.stateService.init();
|
||||||
|
|
||||||
if (!this.popupUtilsService.inPopup(window)) {
|
if (!BrowserPopupUtils.inPopup(window)) {
|
||||||
window.document.body.classList.add("body-full");
|
window.document.body.classList.add("body-full");
|
||||||
} else if (window.screen.availHeight < 600) {
|
} else if (window.screen.availHeight < 600) {
|
||||||
window.document.body.classList.add("body-xs");
|
window.document.body.classList.add("body-xs");
|
||||||
@@ -43,7 +41,7 @@ export class InitService {
|
|||||||
if (
|
if (
|
||||||
this.platformUtilsService.isChrome() &&
|
this.platformUtilsService.isChrome() &&
|
||||||
navigator.platform.indexOf("Mac") > -1 &&
|
navigator.platform.indexOf("Mac") > -1 &&
|
||||||
this.popupUtilsService.inPopup(window) &&
|
BrowserPopupUtils.inPopup(window) &&
|
||||||
(window.screenLeft < 0 ||
|
(window.screenLeft < 0 ||
|
||||||
window.screenTop < 0 ||
|
window.screenTop < 0 ||
|
||||||
window.screenLeft > window.screen.width ||
|
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 MainBackground from "../../background/main.background";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
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 { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserConfigService } from "../../platform/services/browser-config.service";
|
import { BrowserConfigService } from "../../platform/services/browser-config.service";
|
||||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.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 { DebounceNavigationService } from "./debounceNavigationService";
|
||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
|
import { PopupCloseWarningService } from "./popup-close-warning.service";
|
||||||
import { PopupSearchService } from "./popup-search.service";
|
import { PopupSearchService } from "./popup-search.service";
|
||||||
import { PopupUtilsService } from "./popup-utils.service";
|
|
||||||
|
|
||||||
const needsBackgroundInit = BrowserApi.getBackgroundPage() == null;
|
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
|
||||||
const isPrivateMode = needsBackgroundInit && BrowserApi.manifestVersion !== 3;
|
const isPrivateMode = BrowserPopupUtils.inPrivateMode();
|
||||||
const mainBackground: MainBackground = needsBackgroundInit
|
const mainBackground: MainBackground = needsBackgroundInit
|
||||||
? createLocalBgService()
|
? createLocalBgService()
|
||||||
: BrowserApi.getBackgroundPage().bitwardenMain;
|
: BrowserApi.getBackgroundPage().bitwardenMain;
|
||||||
@@ -138,6 +139,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
InitService,
|
InitService,
|
||||||
DebounceNavigationService,
|
DebounceNavigationService,
|
||||||
DialogService,
|
DialogService,
|
||||||
|
PopupCloseWarningService,
|
||||||
{
|
{
|
||||||
provide: LOCALE_ID,
|
provide: LOCALE_ID,
|
||||||
useFactory: () => getBgService<I18nServiceAbstraction>("i18nService")().translationLocale,
|
useFactory: () => getBgService<I18nServiceAbstraction>("i18nService")().translationLocale,
|
||||||
@@ -150,7 +152,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
{ provide: BaseUnauthGuardService, useClass: UnauthGuardService },
|
{ provide: BaseUnauthGuardService, useClass: UnauthGuardService },
|
||||||
{ provide: PopupUtilsService, useFactory: () => new PopupUtilsService(isPrivateMode) },
|
|
||||||
{
|
{
|
||||||
provide: MessagingService,
|
provide: MessagingService,
|
||||||
useFactory: () => {
|
useFactory: () => {
|
||||||
@@ -523,13 +524,10 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: FilePopoutUtilsService,
|
provide: FilePopoutUtilsService,
|
||||||
useFactory: (
|
useFactory: (platformUtilsService: PlatformUtilsService) => {
|
||||||
platformUtilsService: PlatformUtilsService,
|
return new FilePopoutUtilsService(platformUtilsService);
|
||||||
popupUtilsService: PopupUtilsService
|
|
||||||
) => {
|
|
||||||
return new FilePopoutUtilsService(platformUtilsService, popupUtilsService);
|
|
||||||
},
|
},
|
||||||
deps: [PlatformUtilsService, PopupUtilsService],
|
deps: [PlatformUtilsService],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
||||||
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
|
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
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 { AboutComponent } from "./about.component";
|
||||||
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||||
@@ -95,7 +95,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
@@ -335,7 +334,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
if (this.platformUtilsService.isFirefox() && this.popupUtilsService.inSidebar(window)) {
|
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
|
||||||
await this.dialogService.openSimpleDialog({
|
await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "nativeMessaginPermissionSidebarTitle" },
|
title: { key: "nativeMessaginPermissionSidebarTitle" },
|
||||||
content: { key: "nativeMessaginPermissionSidebarDesc" },
|
content: { key: "nativeMessaginPermissionSidebarDesc" },
|
||||||
@@ -474,7 +473,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
async import() {
|
async import() {
|
||||||
await this.router.navigate(["/import"]);
|
await this.router.navigate(["/import"]);
|
||||||
if (await BrowserApi.isPopupOpen()) {
|
if (await BrowserApi.isPopupOpen()) {
|
||||||
this.popupUtilsService.popOut(window);
|
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
|
||||||
import { PopupUtilsService } from "./services/popup-utils.service";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-tabs",
|
selector: "app-tabs",
|
||||||
@@ -9,9 +9,7 @@ import { PopupUtilsService } from "./services/popup-utils.service";
|
|||||||
export class TabsComponent implements OnInit {
|
export class TabsComponent implements OnInit {
|
||||||
showCurrentTab = true;
|
showCurrentTab = true;
|
||||||
|
|
||||||
constructor(private popupUtilsService: PopupUtilsService) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { CalloutModule } from "@bitwarden/components";
|
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";
|
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -19,10 +19,7 @@ export class FilePopoutCalloutComponent implements OnInit {
|
|||||||
protected showSafariFileWarning: boolean;
|
protected showSafariFileWarning: boolean;
|
||||||
protected showChromiumFileWarning: boolean;
|
protected showChromiumFileWarning: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(private filePopoutUtilsService: FilePopoutUtilsService) {}
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
private filePopoutUtilsService: FilePopoutUtilsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.showFilePopoutMessage = this.filePopoutUtilsService.showFilePopoutMessage(window);
|
this.showFilePopoutMessage = this.filePopoutUtilsService.showFilePopoutMessage(window);
|
||||||
@@ -32,6 +29,6 @@ export class FilePopoutCalloutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popOutWindow() {
|
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 { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
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";
|
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -44,7 +44,6 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
sendApiService: SendApiService,
|
sendApiService: SendApiService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
@@ -68,14 +67,14 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popOutWindow() {
|
popOutWindow() {
|
||||||
this.popupUtilsService.popOut(window);
|
BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// File visibility
|
// File visibility
|
||||||
this.showFileSelector =
|
this.showFileSelector =
|
||||||
!this.editMode && !this.filePopoutUtilsService.showFilePopoutMessage(window);
|
!this.editMode && !this.filePopoutUtilsService.showFilePopoutMessage(window);
|
||||||
this.inPopout = this.popupUtilsService.inPopout(window);
|
this.inPopout = BrowserPopupUtils.inPopout(window);
|
||||||
this.isFirefox = this.platformUtilsService.isFirefox();
|
this.isFirefox = this.platformUtilsService.isFirefox();
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// 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 { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
|
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
||||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
|
||||||
|
|
||||||
const ComponentId = "SendComponent";
|
const ComponentId = "SendComponent";
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
|||||||
ngZone: NgZone,
|
ngZone: NgZone,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
searchService: SearchService,
|
searchService: SearchService,
|
||||||
private popupUtils: PopupUtilsService,
|
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
@@ -74,7 +73,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// Determine Header details
|
// Determine Header details
|
||||||
this.showLeftHeader = !(
|
this.showLeftHeader = !(
|
||||||
this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||||
);
|
);
|
||||||
// Clear state of Send Type Component
|
// Clear state of Send Type Component
|
||||||
await this.stateService.setBrowserSendTypeComponentState(null);
|
await this.stateService.setBrowserSendTypeComponentState(null);
|
||||||
@@ -97,7 +96,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
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
|
// Load all sends if sync completed in background
|
||||||
@@ -172,7 +171,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
|
|||||||
|
|
||||||
private async saveState() {
|
private async saveState() {
|
||||||
this.state = Object.assign(new BrowserSendComponentState(), {
|
this.state = Object.assign(new BrowserSendComponentState(), {
|
||||||
scrollY: this.popupUtils.getContentScrollY(window),
|
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||||
searchText: this.searchText,
|
searchText: this.searchText,
|
||||||
sends: this.sends,
|
sends: this.sends,
|
||||||
typeCounts: this.typeCounts,
|
typeCounts: this.typeCounts,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service.
|
|||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserComponentState } from "../../../models/browserComponentState";
|
import { BrowserComponentState } from "../../../models/browserComponentState";
|
||||||
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
||||||
import { PopupUtilsService } from "../../../popup/services/popup-utils.service";
|
|
||||||
|
|
||||||
const ComponentId = "SendTypeComponent";
|
const ComponentId = "SendTypeComponent";
|
||||||
|
|
||||||
@@ -42,7 +42,6 @@ export class SendTypeComponent extends BaseSendComponent {
|
|||||||
ngZone: NgZone,
|
ngZone: NgZone,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
searchService: SearchService,
|
searchService: SearchService,
|
||||||
private popupUtils: PopupUtilsService,
|
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
@@ -102,7 +101,7 @@ export class SendTypeComponent extends BaseSendComponent {
|
|||||||
|
|
||||||
// Restore state and remove reference
|
// Restore state and remove reference
|
||||||
if (this.applySavedState && this.state != null) {
|
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);
|
this.stateService.setBrowserSendTypeComponentState(null);
|
||||||
});
|
});
|
||||||
@@ -163,7 +162,7 @@ export class SendTypeComponent extends BaseSendComponent {
|
|||||||
|
|
||||||
private async saveState() {
|
private async saveState() {
|
||||||
this.state = {
|
this.state = {
|
||||||
scrollY: this.popupUtils.getContentScrollY(window),
|
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||||
searchText: this.searchText,
|
searchText: this.searchText,
|
||||||
};
|
};
|
||||||
await this.stateService.setBrowserSendTypeComponentState(this.state);
|
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 { 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.
|
* Service for determining whether to display file popout callout messages.
|
||||||
@@ -12,10 +12,7 @@ export class FilePopoutUtilsService {
|
|||||||
/**
|
/**
|
||||||
* Creates an instance of FilePopoutUtilsService.
|
* Creates an instance of FilePopoutUtilsService.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private popupUtilsService: PopupUtilsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether to show any file popout callout message in the current browser.
|
* Determines whether to show any file popout callout message in the current browser.
|
||||||
@@ -38,7 +35,7 @@ export class FilePopoutUtilsService {
|
|||||||
showFirefoxFileWarning(win: Window): boolean {
|
showFirefoxFileWarning(win: Window): boolean {
|
||||||
return (
|
return (
|
||||||
this.platformUtilsService.isFirefox() &&
|
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.
|
* @returns True if the extension is not in a popout; otherwise, false.
|
||||||
*/
|
*/
|
||||||
showSafariFileWarning(win: Window): boolean {
|
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 (
|
return (
|
||||||
(this.isLinux(win) || this.isUnsupportedMac(win)) &&
|
(this.isLinux(win) || this.isUnsupportedMac(win)) &&
|
||||||
!this.platformUtilsService.isFirefox() &&
|
!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";
|
} from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
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";
|
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.
|
* 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 {
|
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||||
constructor(
|
constructor(private authService: AuthService) {}
|
||||||
private browserPopoutWindowService: BrowserPopoutWindowService,
|
|
||||||
private authService: AuthService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async newSession(
|
async newSession(
|
||||||
fallbackSupported: boolean,
|
fallbackSupported: boolean,
|
||||||
@@ -127,7 +124,6 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
|
|||||||
abortController?: AbortController
|
abortController?: AbortController
|
||||||
): Promise<Fido2UserInterfaceSession> {
|
): Promise<Fido2UserInterfaceSession> {
|
||||||
return await BrowserFido2UserInterfaceSession.create(
|
return await BrowserFido2UserInterfaceSession.create(
|
||||||
this.browserPopoutWindowService,
|
|
||||||
this.authService,
|
this.authService,
|
||||||
fallbackSupported,
|
fallbackSupported,
|
||||||
tab,
|
tab,
|
||||||
@@ -138,14 +134,12 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
|
|||||||
|
|
||||||
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
||||||
static async create(
|
static async create(
|
||||||
browserPopoutWindowService: BrowserPopoutWindowService,
|
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
fallbackSupported: boolean,
|
fallbackSupported: boolean,
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
abortController?: AbortController
|
abortController?: AbortController
|
||||||
): Promise<BrowserFido2UserInterfaceSession> {
|
): Promise<BrowserFido2UserInterfaceSession> {
|
||||||
return new BrowserFido2UserInterfaceSession(
|
return new BrowserFido2UserInterfaceSession(
|
||||||
browserPopoutWindowService,
|
|
||||||
authService,
|
authService,
|
||||||
fallbackSupported,
|
fallbackSupported,
|
||||||
tab,
|
tab,
|
||||||
@@ -183,7 +177,6 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly browserPopoutWindowService: BrowserPopoutWindowService,
|
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly fallbackSupported: boolean,
|
private readonly fallbackSupported: boolean,
|
||||||
private readonly tab: chrome.tabs.Tab,
|
private readonly tab: chrome.tabs.Tab,
|
||||||
@@ -304,7 +297,7 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
await this.browserPopoutWindowService.closeFido2Popout();
|
await closeFido2Popout(this.sessionId);
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
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,
|
sessionId: this.sessionId,
|
||||||
senderTabId: this.tab.id,
|
|
||||||
fallbackSupported: this.fallbackSupported,
|
fallbackSupported: this.fallbackSupported,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
BrowserFido2Message,
|
BrowserFido2Message,
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||||
|
import { VaultPopoutType } from "../../utils/vault-popout-window";
|
||||||
|
|
||||||
interface ViewData {
|
interface ViewData {
|
||||||
message: BrowserFido2Message;
|
message: BrowserFido2Message;
|
||||||
@@ -280,6 +281,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
uilocation: "popout",
|
uilocation: "popout",
|
||||||
senderTabId: this.senderTabId,
|
senderTabId: this.senderTabId,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -299,6 +301,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
senderTabId: this.senderTabId,
|
senderTabId: this.senderTabId,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
userVerification: data.userVerification,
|
userVerification: data.userVerification,
|
||||||
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
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 {
|
import {
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
fido2PopoutSessionData$,
|
fido2PopoutSessionData$,
|
||||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||||
|
import { VaultPopoutType, closeAddEditVaultItemPopout } from "../../utils/vault-popout-window";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-add-edit",
|
selector: "app-vault-add-edit",
|
||||||
@@ -40,9 +42,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
showAttachments = true;
|
showAttachments = true;
|
||||||
openAttachmentsInPopup: boolean;
|
openAttachmentsInPopup: boolean;
|
||||||
showAutoFillOnPageLoadOptions: boolean;
|
showAutoFillOnPageLoadOptions: boolean;
|
||||||
senderTabId?: number;
|
private singleActionKey: string;
|
||||||
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
|
||||||
inPopout = false;
|
|
||||||
|
|
||||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
private location: Location,
|
private location: Location,
|
||||||
eventCollectionService: EventCollectionService,
|
eventCollectionService: EventCollectionService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
private popupCloseWarningService: PopupCloseWarningService,
|
||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
@@ -91,9 +91,6 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||||
this.senderTabId = parseInt(params?.senderTabId, 10) || undefined;
|
|
||||||
this.uilocation = params?.uilocation;
|
|
||||||
|
|
||||||
if (params.cipherId) {
|
if (params.cipherId) {
|
||||||
this.cipherId = params.cipherId;
|
this.cipherId = params.cipherId;
|
||||||
}
|
}
|
||||||
@@ -119,18 +116,16 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
if (params.selectedVault) {
|
if (params.selectedVault) {
|
||||||
this.organizationId = params.selectedVault;
|
this.organizationId = params.selectedVault;
|
||||||
}
|
}
|
||||||
|
if (params.singleActionKey) {
|
||||||
|
this.singleActionKey = params.singleActionKey;
|
||||||
|
}
|
||||||
await this.load();
|
await this.load();
|
||||||
|
|
||||||
if (!this.editMode || this.cloneMode) {
|
if (!this.editMode || this.cloneMode) {
|
||||||
if (
|
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
|
||||||
!this.popupUtilsService.inPopout(window) &&
|
|
||||||
params.name &&
|
|
||||||
(this.cipher.name == null || this.cipher.name === "")
|
|
||||||
) {
|
|
||||||
this.cipher.name = params.name;
|
this.cipher.name = params.name;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!this.popupUtilsService.inPopout(window) &&
|
|
||||||
params.uri &&
|
params.uri &&
|
||||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].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) {
|
if (!this.editMode) {
|
||||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||||
this.currentUris =
|
this.currentUris =
|
||||||
@@ -153,8 +146,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
|
|
||||||
if (this.popupUtilsService.inTab(window)) {
|
if (BrowserPopupUtils.inPopout(window)) {
|
||||||
this.popupUtilsService.enableCloseTabWarning();
|
this.popupCloseWarningService.enable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +160,10 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
async submit(): Promise<boolean> {
|
async submit(): Promise<boolean> {
|
||||||
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||||
// Would be refactored after rework is done on the windows popout service
|
|
||||||
|
|
||||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
||||||
|
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||||
if (
|
if (
|
||||||
this.inPopout &&
|
inFido2PopoutWindow &&
|
||||||
isFido2Session &&
|
|
||||||
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
@@ -183,7 +174,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inPopout && isFido2Session) {
|
if (BrowserPopupUtils.inPopout(window)) {
|
||||||
|
this.popupCloseWarningService.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFido2PopoutWindow) {
|
||||||
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
|
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
|
||||||
sessionId,
|
sessionId,
|
||||||
this.cipher.id,
|
this.cipher.id,
|
||||||
@@ -192,14 +187,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.popupUtilsService.inTab(window)) {
|
if (this.inAddEditPopoutWindow()) {
|
||||||
this.popupUtilsService.disableCloseTabWarning();
|
await closeAddEditVaultItemPopout(1000);
|
||||||
this.messagingService.send("closeTab", { delay: 1000 });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.senderTabId && this.inPopout) {
|
|
||||||
setTimeout(() => this.close(), 1000);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +208,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
|
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
|
||||||
.toString();
|
.toString();
|
||||||
const currentBaseUrl = window.location.href.replace(this.router.url, "");
|
const currentBaseUrl = window.location.href.replace(this.router.url, "");
|
||||||
this.popupUtilsService.popOut(window, currentBaseUrl + destinationUrl);
|
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
|
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
|
||||||
}
|
}
|
||||||
@@ -235,34 +224,21 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
async cancel() {
|
async cancel() {
|
||||||
super.cancel();
|
super.cancel();
|
||||||
|
|
||||||
// Would be refactored after rework is done on the windows popout service
|
|
||||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||||
if (this.inPopout && sessionData.isFido2Session) {
|
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
|
||||||
|
this.popupCloseWarningService.disable();
|
||||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.senderTabId && this.inPopout) {
|
if (this.inAddEditPopoutWindow()) {
|
||||||
this.close();
|
closeAddEditVaultItemPopout();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.popupUtilsService.inTab(window)) {
|
|
||||||
this.messagingService.send("closeTab");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for closing single-action views
|
|
||||||
close() {
|
|
||||||
BrowserApi.focusTab(this.senderTabId);
|
|
||||||
window.close();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateUsername(): Promise<boolean> {
|
async generateUsername(): Promise<boolean> {
|
||||||
const confirmed = await super.generateUsername();
|
const confirmed = await super.generateUsername();
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
@@ -356,4 +332,11 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
this.i18nService.t("autofillOnPageLoadSetToDefault")
|
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 { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
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";
|
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "CurrentTabComponent";
|
const BroadcasterSubscriptionId = "CurrentTabComponent";
|
||||||
@@ -55,7 +55,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
private autofillService: AutofillService,
|
private autofillService: AutofillService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -72,7 +71,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||||
this.inSidebar = this.popupUtilsService.inSidebar(window);
|
this.inSidebar = BrowserPopupUtils.inSidebar(window);
|
||||||
|
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||||
this.ngZone.run(async () => {
|
this.ngZone.run(async () => {
|
||||||
@@ -185,7 +184,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||||||
if (this.totpCode != null) {
|
if (this.totpCode != null) {
|
||||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||||
}
|
}
|
||||||
if (this.popupUtilsService.inPopup(window)) {
|
if (BrowserPopupUtils.inPopup(window)) {
|
||||||
if (!closePopupDelay) {
|
if (!closePopupDelay) {
|
||||||
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
|
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
|
||||||
BrowserApi.closePopup(window);
|
BrowserApi.closePopup(window);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
|||||||
|
|
||||||
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
||||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
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 { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
|
||||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||||
|
|
||||||
const ComponentId = "VaultComponent";
|
const ComponentId = "VaultComponent";
|
||||||
@@ -80,7 +80,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private popupUtils: PopupUtilsService,
|
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
@@ -94,7 +93,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||||
this.showLeftHeader = !(
|
this.showLeftHeader = !(
|
||||||
this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||||
);
|
);
|
||||||
await this.browserStateService.setBrowserVaultItemsComponentState(null);
|
await this.browserStateService.setBrowserVaultItemsComponentState(null);
|
||||||
|
|
||||||
@@ -136,7 +135,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
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;
|
this.preventSelected = true;
|
||||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||||
if (this.popupUtils.inPopup(window)) {
|
if (BrowserPopupUtils.inPopup(window)) {
|
||||||
BrowserApi.closePopup(window);
|
BrowserApi.closePopup(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +375,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private async saveState() {
|
private async saveState() {
|
||||||
this.state = Object.assign(new BrowserGroupingsComponentState(), {
|
this.state = Object.assign(new BrowserGroupingsComponentState(), {
|
||||||
scrollY: this.popupUtils.getContentScrollY(window),
|
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||||
searchText: this.searchText,
|
searchText: this.searchText,
|
||||||
favoriteCiphers: this.favoriteCiphers,
|
favoriteCiphers: this.favoriteCiphers,
|
||||||
noFolderCiphers: this.noFolderCiphers,
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.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 { BrowserComponentState } from "../../../../models/browserComponentState";
|
||||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
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 { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
|
||||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||||
|
|
||||||
const ComponentId = "VaultItemsComponent";
|
const ComponentId = "VaultItemsComponent";
|
||||||
@@ -61,9 +60,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
private popupUtils: PopupUtilsService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private folderService: FolderService,
|
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
@@ -155,11 +152,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.applySavedState && this.state != null) {
|
if (this.applySavedState && this.state != null) {
|
||||||
window.setTimeout(
|
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
|
||||||
() =>
|
delay: 0,
|
||||||
this.popupUtils.setContentScrollY(window, this.state.scrollY, this.scrollingContainer),
|
containerSelector: this.scrollingContainer,
|
||||||
0
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await this.stateService.setBrowserVaultItemsComponentState(null);
|
await this.stateService.setBrowserVaultItemsComponentState(null);
|
||||||
});
|
});
|
||||||
@@ -219,7 +215,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||||||
this.preventSelected = true;
|
this.preventSelected = true;
|
||||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||||
if (this.popupUtils.inPopup(window)) {
|
if (BrowserPopupUtils.inPopup(window)) {
|
||||||
BrowserApi.closePopup(window);
|
BrowserApi.closePopup(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +284,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||||||
|
|
||||||
private async saveState() {
|
private async saveState() {
|
||||||
this.state = {
|
this.state = {
|
||||||
scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer),
|
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
|
||||||
searchText: this.searchText,
|
searchText: this.searchText,
|
||||||
};
|
};
|
||||||
await this.stateService.setBrowserVaultItemsComponentState(this.state);
|
await this.stateService.setBrowserVaultItemsComponentState(this.state);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
|||||||
|
|
||||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import { PopupUtilsService } from "../../../../popup/services/popup-utils.service";
|
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||||
import {
|
import {
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
fido2PopoutSessionData$,
|
fido2PopoutSessionData$,
|
||||||
@@ -84,7 +84,6 @@ export class ViewComponent extends BaseViewComponent {
|
|||||||
eventCollectionService: EventCollectionService,
|
eventCollectionService: EventCollectionService,
|
||||||
private autofillService: AutofillService,
|
private autofillService: AutofillService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private popupUtilsService: PopupUtilsService,
|
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
@@ -121,7 +120,7 @@ export class ViewComponent extends BaseViewComponent {
|
|||||||
this.uilocation = value?.uilocation;
|
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
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
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(),
|
sendMessage: jest.fn(),
|
||||||
getManifest: jest.fn(),
|
getManifest: jest.fn(),
|
||||||
|
getURL: jest.fn((path) => `chrome-extension://id/${path}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextMenus = {
|
const contextMenus = {
|
||||||
@@ -37,12 +38,21 @@ const i18n = {
|
|||||||
const tabs = {
|
const tabs = {
|
||||||
executeScript: jest.fn(),
|
executeScript: jest.fn(),
|
||||||
sendMessage: jest.fn(),
|
sendMessage: jest.fn(),
|
||||||
|
query: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const scripting = {
|
const scripting = {
|
||||||
executeScript: jest.fn(),
|
executeScript: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const windows = {
|
||||||
|
create: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
getCurrent: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
// set chrome
|
// set chrome
|
||||||
global.chrome = {
|
global.chrome = {
|
||||||
i18n,
|
i18n,
|
||||||
@@ -51,4 +61,5 @@ global.chrome = {
|
|||||||
contextMenus,
|
contextMenus,
|
||||||
tabs,
|
tabs,
|
||||||
scripting,
|
scripting,
|
||||||
|
windows,
|
||||||
} as any;
|
} as any;
|
||||||
|
|||||||
Reference in New Issue
Block a user