mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 10:23:52 +00:00
PM-27310: WIP – rough proof of concept to replace iframe based notification bar with Shadow DOM rendering
This commit is contained in:
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
|
||||
import { getNotificationTestId } from "../../../../../notification/bar";
|
||||
import { getNotificationTestId } from "../../../../../notification/notification-bar-helpers";
|
||||
import {
|
||||
AtRiskNotification,
|
||||
AtRiskNotificationProps,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NotificationTypes } from "../../../../../notification/abstractions/noti
|
||||
import {
|
||||
getConfirmationHeaderMessage,
|
||||
getNotificationTestId,
|
||||
} from "../../../../../notification/bar";
|
||||
} from "../../../../../notification/notification-bar-helpers";
|
||||
import {
|
||||
NotificationConfirmationContainer,
|
||||
NotificationConfirmationContainerProps,
|
||||
|
||||
@@ -5,7 +5,10 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
|
||||
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
|
||||
import { getNotificationHeaderMessage, getNotificationTestId } from "../../../../notification/bar";
|
||||
import {
|
||||
getNotificationHeaderMessage,
|
||||
getNotificationTestId,
|
||||
} from "../../../../notification/notification-bar-helpers";
|
||||
import { NotificationContainer, NotificationContainerProps } from "../../notification/container";
|
||||
import { mockBrowserI18nGetMessage, mockI18n } from "../mock-data";
|
||||
|
||||
|
||||
@@ -160,7 +160,9 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export default customElements.define(optionSelectionTagName, OptionSelection);
|
||||
if (!globalThis.customElements?.get(optionSelectionTagName)) {
|
||||
globalThis.customElements?.define(optionSelectionTagName, OptionSelection);
|
||||
}
|
||||
|
||||
function getDefaultOption(options: Option[] = []) {
|
||||
return options.find((option: Option) => option.default) || options[0];
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Bitwarden</title>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,121 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { postWindowMessage } from "../spec/testing-utils";
|
||||
|
||||
import { NotificationBarWindowMessage } from "./abstractions/notification-bar";
|
||||
import "./bar";
|
||||
|
||||
jest.mock("lit", () => ({ render: jest.fn() }));
|
||||
jest.mock("@lit-labs/signals", () => ({
|
||||
signal: jest.fn((testValue) => ({ get: (): typeof testValue => testValue })),
|
||||
}));
|
||||
jest.mock("../content/components/notification/container", () => ({
|
||||
NotificationContainer: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("NotificationBar iframe handleWindowMessage security", () => {
|
||||
const trustedOrigin = "http://localhost";
|
||||
const maliciousOrigin = "https://malicious.com";
|
||||
|
||||
const createMessage = (
|
||||
overrides: Partial<NotificationBarWindowMessage> = {},
|
||||
): NotificationBarWindowMessage => ({
|
||||
command: "initNotificationBar",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(globalThis, "location", {
|
||||
value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(globalThis, "parent", {
|
||||
value: mock<Window>(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.dispatchEvent(new Event("load"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: "not from parent window",
|
||||
message: () => createMessage(),
|
||||
origin: trustedOrigin,
|
||||
source: () => mock<Window>(),
|
||||
},
|
||||
{
|
||||
description: "with mismatched origin",
|
||||
message: () => createMessage(),
|
||||
origin: maliciousOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "without command field",
|
||||
message: () => ({}),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "initNotificationBar with mismatched parentOrigin",
|
||||
message: () => createMessage({ parentOrigin: maliciousOrigin }),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
{
|
||||
description: "when windowMessageOrigin is not set",
|
||||
message: () => createMessage(),
|
||||
origin: "different-origin",
|
||||
source: () => globalThis.parent,
|
||||
resetOrigin: true,
|
||||
},
|
||||
{
|
||||
description: "with null source",
|
||||
message: () => createMessage(),
|
||||
origin: trustedOrigin,
|
||||
source: (): null => null,
|
||||
},
|
||||
{
|
||||
description: "with unknown command",
|
||||
message: () => createMessage({ command: "unknownCommand" }),
|
||||
origin: trustedOrigin,
|
||||
source: () => globalThis.parent,
|
||||
},
|
||||
])("should reject messages $description", ({ message, origin, source, resetOrigin }) => {
|
||||
if (resetOrigin) {
|
||||
Object.defineProperty(globalThis, "location", {
|
||||
value: { search: "" },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||
postWindowMessage(message(), origin, source());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should accept and handle valid trusted messages", () => {
|
||||
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||
spy.mockClear();
|
||||
|
||||
const validMessage = createMessage({
|
||||
parentOrigin: trustedOrigin,
|
||||
initData: {
|
||||
type: "change",
|
||||
isVaultLocked: false,
|
||||
removeIndividualVault: false,
|
||||
importType: null,
|
||||
launchTimestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
postWindowMessage(validMessage, trustedOrigin, globalThis.parent);
|
||||
expect(validMessage.command).toBe("initNotificationBar");
|
||||
expect(validMessage.parentOrigin).toBe(trustedOrigin);
|
||||
expect(validMessage.initData).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,457 +0,0 @@
|
||||
import { render } from "lit";
|
||||
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { NotificationCipherData } from "../content/components/cipher/types";
|
||||
import { CollectionView, I18n, OrgView } from "../content/components/common-types";
|
||||
import { AtRiskNotification } from "../content/components/notification/at-risk-password/container";
|
||||
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
|
||||
import { NotificationContainer } from "../content/components/notification/container";
|
||||
import { selectedCipher as selectedCipherSignal } from "../content/components/signals/selected-cipher";
|
||||
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
||||
import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
|
||||
|
||||
import {
|
||||
NotificationBarWindowMessageHandlers,
|
||||
NotificationBarWindowMessage,
|
||||
NotificationBarIframeInitData,
|
||||
NotificationType,
|
||||
NotificationTypes,
|
||||
} from "./abstractions/notification-bar";
|
||||
|
||||
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
||||
let windowMessageOrigin: string;
|
||||
|
||||
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||
const trustedParentOrigin = urlParams.get("parentOrigin");
|
||||
|
||||
if (trustedParentOrigin) {
|
||||
windowMessageOrigin = trustedParentOrigin;
|
||||
}
|
||||
|
||||
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
||||
initNotificationBar: ({ message }) => initNotificationBar(message),
|
||||
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
||||
};
|
||||
|
||||
globalThis.addEventListener("load", load);
|
||||
|
||||
function load() {
|
||||
setupWindowMessageListener();
|
||||
postMessageToParent({ command: "initNotificationBar" });
|
||||
}
|
||||
|
||||
function getI18n() {
|
||||
return {
|
||||
appName: chrome.i18n.getMessage("appName"),
|
||||
atRiskPassword: chrome.i18n.getMessage("atRiskPassword"),
|
||||
changePassword: chrome.i18n.getMessage("changePassword"),
|
||||
close: chrome.i18n.getMessage("close"),
|
||||
collection: chrome.i18n.getMessage("collection"),
|
||||
folder: chrome.i18n.getMessage("folder"),
|
||||
loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"),
|
||||
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
|
||||
loginUpdatedConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"),
|
||||
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
|
||||
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
|
||||
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
|
||||
myVault: chrome.i18n.getMessage("myVault"),
|
||||
never: chrome.i18n.getMessage("never"),
|
||||
newItem: chrome.i18n.getMessage("newItem"),
|
||||
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
|
||||
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
|
||||
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
||||
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
|
||||
notificationLoginSaveConfirmation: chrome.i18n.getMessage("notificationLoginSaveConfirmation"),
|
||||
notificationLoginUpdatedConfirmation: chrome.i18n.getMessage(
|
||||
"notificationLoginUpdatedConfirmation",
|
||||
),
|
||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
||||
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
|
||||
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
|
||||
saveAction: chrome.i18n.getMessage("notificationAddSave"),
|
||||
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
|
||||
saveFailure: chrome.i18n.getMessage("saveFailure"),
|
||||
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
|
||||
saveLogin: chrome.i18n.getMessage("saveLogin"),
|
||||
typeLogin: chrome.i18n.getMessage("typeLogin"),
|
||||
unlockToSave: chrome.i18n.getMessage("unlockToSave"),
|
||||
updateLogin: chrome.i18n.getMessage("updateLogin"),
|
||||
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
|
||||
vault: chrome.i18n.getMessage("vault"),
|
||||
view: chrome.i18n.getMessage("view"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized header message for the notification bar based on the notification type.
|
||||
*
|
||||
* @returns The localized header message string, or undefined if the type is not recognized.
|
||||
*/
|
||||
export function getNotificationHeaderMessage(i18n: I18n, type?: NotificationType) {
|
||||
return type
|
||||
? {
|
||||
[NotificationTypes.Add]: i18n.saveLogin,
|
||||
[NotificationTypes.Change]: i18n.updateLogin,
|
||||
[NotificationTypes.Unlock]: i18n.unlockToSave,
|
||||
[NotificationTypes.AtRiskPassword]: i18n.atRiskPassword,
|
||||
}[type]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized header message for the confirmation message bar based on the notification type.
|
||||
*
|
||||
* @returns The localized header message string, or undefined if the type is not recognized.
|
||||
*/
|
||||
export function getConfirmationHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) {
|
||||
if (error) {
|
||||
return i18n.saveFailure;
|
||||
}
|
||||
|
||||
return type
|
||||
? {
|
||||
[NotificationTypes.Add]: i18n.loginSaveSuccess,
|
||||
[NotificationTypes.Change]: i18n.loginUpdateSuccess,
|
||||
[NotificationTypes.Unlock]: "",
|
||||
[NotificationTypes.AtRiskPassword]: "",
|
||||
}[type]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the header message to the document title.
|
||||
* If the header message is already present, it avoids duplication.
|
||||
*/
|
||||
export function appendHeaderMessageToTitle(headerMessage?: string) {
|
||||
if (!headerMessage) {
|
||||
return;
|
||||
}
|
||||
const baseTitle = document.title.split(" - ")[0];
|
||||
document.title = `${baseTitle} - ${headerMessage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the effective notification type to use based on initialization data.
|
||||
*
|
||||
* If the vault is locked, the notification type will be set to `Unlock`.
|
||||
* Otherwise, the type provided in the init data is returned.
|
||||
*
|
||||
* @returns The resolved `NotificationType` to be used for rendering logic.
|
||||
*/
|
||||
function resolveNotificationType(initData: NotificationBarIframeInitData): NotificationType {
|
||||
if (initData.isVaultLocked) {
|
||||
return NotificationTypes.Unlock;
|
||||
}
|
||||
|
||||
return initData.type as NotificationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate test ID based on the resolved notification type.
|
||||
*
|
||||
* @param type - The resolved NotificationType.
|
||||
* @param isConfirmation - Optional flag for confirmation vs. notification container.
|
||||
*/
|
||||
export function getNotificationTestId(
|
||||
notificationType: NotificationType,
|
||||
isConfirmation = false,
|
||||
): string {
|
||||
if (isConfirmation) {
|
||||
return "confirmation-notification-bar";
|
||||
}
|
||||
|
||||
return {
|
||||
[NotificationTypes.Unlock]: "unlock-notification-bar",
|
||||
[NotificationTypes.Add]: "save-notification-bar",
|
||||
[NotificationTypes.Change]: "update-notification-bar",
|
||||
[NotificationTypes.AtRiskPassword]: "at-risk-notification-bar",
|
||||
}[notificationType];
|
||||
}
|
||||
|
||||
async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const { initData } = message;
|
||||
if (!initData) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationBarIframeInitData = initData;
|
||||
const {
|
||||
isVaultLocked,
|
||||
removeIndividualVault: personalVaultDisallowed,
|
||||
theme,
|
||||
} = notificationBarIframeInitData;
|
||||
const i18n = getI18n();
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
|
||||
const notificationType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getNotificationHeaderMessage(i18n, notificationType);
|
||||
const notificationTestId = getNotificationTestId(notificationType);
|
||||
appendHeaderMessageToTitle(headerMessage);
|
||||
|
||||
if (isVaultLocked) {
|
||||
const notificationConfig = {
|
||||
...notificationBarIframeInitData,
|
||||
headerMessage,
|
||||
type: notificationType,
|
||||
notificationTestId,
|
||||
theme: resolvedTheme,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification,
|
||||
handleEditOrUpdateAction,
|
||||
i18n,
|
||||
};
|
||||
|
||||
const handleSaveAction = () => {
|
||||
// cipher ID is null while vault is locked.
|
||||
sendSaveCipherMessage(null, true);
|
||||
|
||||
render(
|
||||
NotificationContainer({
|
||||
...notificationConfig,
|
||||
handleSaveAction: () => {},
|
||||
isLoading: true,
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
const UnlockNotification = NotificationContainer({ ...notificationConfig, handleSaveAction });
|
||||
|
||||
return render(UnlockNotification, document.body);
|
||||
}
|
||||
|
||||
// Handle AtRiskPasswordNotification render
|
||||
if (notificationBarIframeInitData.type === NotificationTypes.AtRiskPassword) {
|
||||
return render(
|
||||
AtRiskNotification({
|
||||
...notificationBarIframeInitData,
|
||||
type: notificationBarIframeInitData.type as NotificationType,
|
||||
theme: resolvedTheme,
|
||||
i18n,
|
||||
notificationTestId,
|
||||
params: initData.params,
|
||||
handleCloseNotification,
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
// Default scenario: add or update password
|
||||
const orgId = selectedVaultSignal.get();
|
||||
|
||||
await Promise.all([
|
||||
new Promise<OrgView[]>((resolve) => sendPlatformMessage({ command: "bgGetOrgData" }, resolve)),
|
||||
new Promise<FolderView[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetFolderData" }, resolve),
|
||||
),
|
||||
new Promise<NotificationCipherData[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve),
|
||||
),
|
||||
new Promise<CollectionView[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetCollectionData", orgId }, resolve),
|
||||
),
|
||||
]).then(([organizations, folders, ciphers, collections]) => {
|
||||
notificationBarIframeInitData = {
|
||||
...notificationBarIframeInitData,
|
||||
organizations,
|
||||
folders,
|
||||
ciphers,
|
||||
collections,
|
||||
};
|
||||
|
||||
// @TODO use context to avoid prop drilling
|
||||
return render(
|
||||
NotificationContainer({
|
||||
...notificationBarIframeInitData,
|
||||
headerMessage,
|
||||
type: notificationType,
|
||||
theme: resolvedTheme,
|
||||
notificationTestId,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification,
|
||||
handleSaveAction,
|
||||
handleEditOrUpdateAction,
|
||||
i18n,
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
});
|
||||
|
||||
function handleEditOrUpdateAction(e: Event) {
|
||||
e.preventDefault();
|
||||
sendSaveCipherMessage(selectedCipherSignal.get(), notificationType === NotificationTypes.Add);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseNotification(e: Event) {
|
||||
e.preventDefault();
|
||||
sendPlatformMessage({
|
||||
command: "bgCloseNotificationBar",
|
||||
fadeOutNotification: true,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSaveAction(e: Event) {
|
||||
const selectedCipher = selectedCipherSignal.get();
|
||||
const selectedVault = selectedVaultSignal.get();
|
||||
const selectedFolder = selectedFolderSignal.get();
|
||||
|
||||
if (selectedVault.length > 1) {
|
||||
openAddEditVaultItemPopout(e, {
|
||||
organizationId: selectedVault,
|
||||
...(selectedFolder?.length > 1 ? { folder: selectedFolder } : {}),
|
||||
});
|
||||
handleCloseNotification(e);
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
sendSaveCipherMessage(selectedCipher, removeIndividualVault(), selectedFolder);
|
||||
if (removeIndividualVault()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function sendSaveCipherMessage(cipherId: CipherView["id"] | null, edit: boolean, folder?: string) {
|
||||
sendPlatformMessage({
|
||||
command: "bgSaveCipher",
|
||||
cipherId,
|
||||
folder,
|
||||
edit,
|
||||
});
|
||||
}
|
||||
|
||||
function openAddEditVaultItemPopout(
|
||||
e: Event,
|
||||
options: {
|
||||
cipherId?: string;
|
||||
organizationId?: string;
|
||||
folder?: string;
|
||||
},
|
||||
) {
|
||||
e.preventDefault();
|
||||
sendPlatformMessage({
|
||||
command: "bgOpenAddEditVaultItemPopout",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function openViewVaultItemPopout(cipherId: string) {
|
||||
sendPlatformMessage({
|
||||
command: "bgOpenViewVaultItemPopout",
|
||||
cipherId,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
||||
const { theme, type } = notificationBarIframeInitData;
|
||||
const { error, data } = message;
|
||||
const { cipherId, task, itemName } = data || {};
|
||||
const i18n = getI18n();
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
const resolvedType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error);
|
||||
const notificationTestId = getNotificationTestId(resolvedType, true);
|
||||
appendHeaderMessageToTitle(headerMessage);
|
||||
|
||||
globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
|
||||
|
||||
return render(
|
||||
NotificationConfirmationContainer({
|
||||
...notificationBarIframeInitData,
|
||||
error,
|
||||
handleCloseNotification,
|
||||
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRiskPasswords" }),
|
||||
handleOpenVault: (e: Event) =>
|
||||
cipherId ? openViewVaultItemPopout(cipherId) : openAddEditVaultItemPopout(e, {}),
|
||||
headerMessage,
|
||||
i18n,
|
||||
itemName: itemName ?? i18n.typeLogin,
|
||||
notificationTestId,
|
||||
task,
|
||||
theme: resolvedTheme,
|
||||
type: type as NotificationType,
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
function sendPlatformMessage(
|
||||
msg: Record<string, unknown>,
|
||||
responseCallback?: (response: any) => void,
|
||||
) {
|
||||
chrome.runtime.sendMessage(msg, (response) => {
|
||||
if (responseCallback) {
|
||||
responseCallback(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeIndividualVault(): boolean {
|
||||
return Boolean(notificationBarIframeInitData?.removeIndividualVault);
|
||||
}
|
||||
|
||||
function setupWindowMessageListener() {
|
||||
globalThis.addEventListener("message", handleWindowMessage);
|
||||
}
|
||||
|
||||
function handleWindowMessage(event: MessageEvent) {
|
||||
if (event?.source !== globalThis.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data as NotificationBarWindowMessage;
|
||||
if (!message?.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!windowMessageOrigin || event.origin !== windowMessageOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
message.command === "initNotificationBar" &&
|
||||
message.parentOrigin &&
|
||||
message.parentOrigin !== event.origin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = notificationBarWindowMessageHandlers[message.command];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler({ message });
|
||||
}
|
||||
|
||||
function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]) {
|
||||
if (theme === ThemeTypes.System) {
|
||||
return globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeTypes.Dark
|
||||
: ThemeTypes.Light;
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
function getResolvedTheme(theme: Theme) {
|
||||
const themeType = getTheme(globalThis, theme);
|
||||
|
||||
// There are other possible passed theme values, but for now, resolve to dark or light
|
||||
const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
|
||||
return resolvedTheme;
|
||||
}
|
||||
|
||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||
if (!windowMessageOrigin) {
|
||||
return;
|
||||
}
|
||||
globalThis.parent.postMessage(message, windowMessageOrigin);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { I18n } from "../content/components/common-types";
|
||||
|
||||
import {
|
||||
NotificationBarIframeInitData,
|
||||
NotificationType,
|
||||
NotificationTypes,
|
||||
} from "./abstractions/notification-bar";
|
||||
|
||||
export function getNotificationHeaderMessage(i18n: I18n, type?: NotificationType) {
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
const messages = {
|
||||
[NotificationTypes.Add]: i18n.saveLogin,
|
||||
[NotificationTypes.Change]: i18n.updateLogin,
|
||||
[NotificationTypes.Unlock]: i18n.unlockToSave,
|
||||
[NotificationTypes.AtRiskPassword]: i18n.atRiskPassword,
|
||||
};
|
||||
return messages[type];
|
||||
}
|
||||
|
||||
export function getConfirmationHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) {
|
||||
if (error) {
|
||||
return i18n.saveFailure;
|
||||
}
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
const messages = {
|
||||
[NotificationTypes.Add]: i18n.loginSaveSuccess,
|
||||
[NotificationTypes.Change]: i18n.loginUpdateSuccess,
|
||||
[NotificationTypes.Unlock]: "",
|
||||
[NotificationTypes.AtRiskPassword]: "",
|
||||
};
|
||||
return messages[type];
|
||||
}
|
||||
|
||||
export function getNotificationTestId(notificationType: NotificationType, isConfirmation = false) {
|
||||
if (isConfirmation) {
|
||||
return "confirmation-notification-bar";
|
||||
}
|
||||
const testIds = {
|
||||
[NotificationTypes.Unlock]: "unlock-notification-bar",
|
||||
[NotificationTypes.Add]: "save-notification-bar",
|
||||
[NotificationTypes.Change]: "update-notification-bar",
|
||||
[NotificationTypes.AtRiskPassword]: "at-risk-notification-bar",
|
||||
};
|
||||
return testIds[notificationType];
|
||||
}
|
||||
|
||||
export function resolveNotificationType(initData: NotificationBarIframeInitData): NotificationType {
|
||||
return initData.isVaultLocked ? NotificationTypes.Unlock : (initData.type as NotificationType);
|
||||
}
|
||||
|
||||
export function getResolvedTheme(theme: Theme): Theme {
|
||||
if (theme === ThemeTypes.System) {
|
||||
return globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeTypes.Dark
|
||||
: ThemeTypes.Light;
|
||||
}
|
||||
return theme === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationTaskInfo } from "../../../notification/abstractions/notification-bar";
|
||||
|
||||
export type NotificationTypeData = {
|
||||
isVaultLocked?: boolean;
|
||||
theme?: Theme;
|
||||
@@ -8,19 +10,38 @@ export type NotificationTypeData = {
|
||||
launchTimestamp?: number;
|
||||
};
|
||||
|
||||
export type NotificationsExtensionMessage = {
|
||||
command: string;
|
||||
data?: {
|
||||
type?: string;
|
||||
typeData?: NotificationTypeData;
|
||||
height?: number;
|
||||
error?: string;
|
||||
closedByUser?: boolean;
|
||||
fadeOutNotification?: boolean;
|
||||
params: object;
|
||||
type OpenNotificationBarMessage = {
|
||||
command: "openNotificationBar";
|
||||
data: {
|
||||
type: string;
|
||||
typeData: NotificationTypeData;
|
||||
params?: object;
|
||||
};
|
||||
};
|
||||
|
||||
type CloseNotificationBarMessage = {
|
||||
command: "closeNotificationBar";
|
||||
data?: {
|
||||
closedByUser?: boolean;
|
||||
fadeOutNotification?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type SaveCipherAttemptCompletedMessage = {
|
||||
command: "saveCipherAttemptCompleted";
|
||||
data?: {
|
||||
error?: string;
|
||||
cipherId?: string;
|
||||
task?: NotificationTaskInfo;
|
||||
itemName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type NotificationsExtensionMessage =
|
||||
| OpenNotificationBarMessage
|
||||
| CloseNotificationBarMessage
|
||||
| SaveCipherAttemptCompletedMessage;
|
||||
|
||||
type OverlayNotificationsExtensionMessageParam = {
|
||||
message: NotificationsExtensionMessage;
|
||||
};
|
||||
@@ -34,7 +55,6 @@ export type OverlayNotificationsExtensionMessageHandlers = {
|
||||
[key: string]: ({ message, sender }: OverlayNotificationsExtensionMessageParams) => any;
|
||||
openNotificationBar: ({ message }: OverlayNotificationsExtensionMessageParam) => void;
|
||||
closeNotificationBar: ({ message }: OverlayNotificationsExtensionMessageParam) => void;
|
||||
adjustNotificationBar: ({ message }: OverlayNotificationsExtensionMessageParam) => void;
|
||||
saveCipherAttemptCompleted: ({ message }: OverlayNotificationsExtensionMessageParam) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body within a shadow root 1`] = `
|
||||
<div
|
||||
id="bit-notification-bar"
|
||||
style="height: 400px !important; width: 430px !important; max-width: calc(100% - 20px) !important; min-height: initial !important; top: 10px !important; right: 0px !important; padding: 0px !important; position: fixed !important; z-index: 2147483647 !important; visibility: visible !important; border-radius: 4px !important; background-color: transparent !important; overflow: hidden !important; transition: box-shadow 0.15s ease !important; transition-delay: 0.15s;"
|
||||
>
|
||||
<iframe
|
||||
id="bit-notification-bar-iframe"
|
||||
src="chrome-extension://id/notification/bar.html?parentOrigin=http%3A%2F%2Flocalhost"
|
||||
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,13 +1,29 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { render } from "lit";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { I18n } from "../../../content/components/common-types";
|
||||
import { AtRiskNotification } from "../../../content/components/notification/at-risk-password/container";
|
||||
import { NotificationConfirmationContainer } from "../../../content/components/notification/confirmation/container";
|
||||
import { NotificationContainer } from "../../../content/components/notification/container";
|
||||
import { selectedCipher as selectedCipherSignal } from "../../../content/components/signals/selected-cipher";
|
||||
import { selectedFolder as selectedFolderSignal } from "../../../content/components/signals/selected-folder";
|
||||
import { selectedVault as selectedVaultSignal } from "../../../content/components/signals/selected-vault";
|
||||
import {
|
||||
NotificationBarIframeInitData,
|
||||
NotificationTaskInfo,
|
||||
NotificationType,
|
||||
NotificationTypes,
|
||||
} from "../../../notification/abstractions/notification-bar";
|
||||
import {
|
||||
getConfirmationHeaderMessage,
|
||||
getNotificationHeaderMessage,
|
||||
getNotificationTestId,
|
||||
getResolvedTheme,
|
||||
resolveNotificationType,
|
||||
} from "../../../notification/notification-bar-helpers";
|
||||
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
||||
import {
|
||||
NotificationsExtensionMessage,
|
||||
@@ -15,55 +31,60 @@ import {
|
||||
OverlayNotificationsExtensionMessageHandlers,
|
||||
} from "../abstractions/overlay-notifications-content.service";
|
||||
|
||||
const notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "400px",
|
||||
width: "430px",
|
||||
maxWidth: "calc(100% - 20px)",
|
||||
minHeight: "initial",
|
||||
top: "10px",
|
||||
right: "0px",
|
||||
padding: "0",
|
||||
position: "fixed",
|
||||
zIndex: "2147483647",
|
||||
visibility: "visible",
|
||||
borderRadius: "4px",
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
overflow: "hidden",
|
||||
transition: "box-shadow 0.15s ease, transform 0.15s ease-out, opacity 0.15s ease",
|
||||
transitionDelay: "0.15s",
|
||||
transform: "translateX(100%)",
|
||||
opacity: "0",
|
||||
};
|
||||
|
||||
const emotionStyleSelctor = "style[data-emotion], style[data-emotion-css]";
|
||||
const validNotificationTypes = new Set([
|
||||
NotificationTypes.Add,
|
||||
NotificationTypes.Change,
|
||||
NotificationTypes.Unlock,
|
||||
NotificationTypes.AtRiskPassword,
|
||||
]);
|
||||
|
||||
export class OverlayNotificationsContentService
|
||||
implements OverlayNotificationsContentServiceInterface
|
||||
{
|
||||
private notificationBarRootElement: HTMLElement | null = null;
|
||||
private notificationBarElement: HTMLElement | null = null;
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||
private currentNotificationBarType: NotificationType | null = null;
|
||||
private readonly extensionOrigin: string;
|
||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "400px",
|
||||
width: "430px",
|
||||
maxWidth: "calc(100% - 20px)",
|
||||
minHeight: "initial",
|
||||
top: "10px",
|
||||
right: "0px",
|
||||
padding: "0",
|
||||
position: "fixed",
|
||||
zIndex: "2147483647",
|
||||
visibility: "visible",
|
||||
borderRadius: "4px",
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
overflow: "hidden",
|
||||
transition: "box-shadow 0.15s ease",
|
||||
transitionDelay: "0.15s",
|
||||
};
|
||||
|
||||
private notificationBarIframeElementStyles: Partial<CSSStyleDeclaration> = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "0",
|
||||
display: "block",
|
||||
position: "relative",
|
||||
transition: "transform 0.15s ease-out, opacity 0.15s ease",
|
||||
borderRadius: "4px",
|
||||
colorScheme: "auto",
|
||||
private currentNotificationBarInitData: NotificationBarIframeInitData | null = null;
|
||||
private mutationObserver: MutationObserver | null = null;
|
||||
private foreignMutationsCount = 0;
|
||||
private mutationObserverIterations = 0;
|
||||
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout | null = null;
|
||||
private expectedContainerStyles: Partial<CSSStyleDeclaration> = {};
|
||||
private readonly defaultNotificationBarAttributes: Record<string, string> = {
|
||||
id: "bit-notification-bar",
|
||||
};
|
||||
|
||||
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
|
||||
openNotificationBar: ({ message }) => this.handleOpenNotificationBarMessage(message),
|
||||
closeNotificationBar: ({ message }) => this.handleCloseNotificationBarMessage(message),
|
||||
adjustNotificationBar: ({ message }) => this.handleAdjustNotificationBarHeightMessage(message),
|
||||
saveCipherAttemptCompleted: ({ message }) =>
|
||||
this.handleSaveCipherAttemptCompletedMessage(message),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||
void sendExtensionMessage("checkNotificationQueue");
|
||||
}
|
||||
|
||||
@@ -81,16 +102,20 @@ export class OverlayNotificationsContentService
|
||||
* @param message - The message containing the initialization data for the notification bar.
|
||||
*/
|
||||
private async handleOpenNotificationBarMessage(message: NotificationsExtensionMessage) {
|
||||
if (!message.data) {
|
||||
if (message.command !== "openNotificationBar" || !message.data) {
|
||||
return;
|
||||
}
|
||||
const { type, typeData, params } = message.data;
|
||||
|
||||
if (!validNotificationTypes.has(type as NotificationType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
|
||||
this.closeNotificationBar();
|
||||
}
|
||||
|
||||
const initData = {
|
||||
const initData: NotificationBarIframeInitData = {
|
||||
type: type as NotificationType,
|
||||
isVaultLocked: typeData.isVaultLocked,
|
||||
theme: typeData.theme,
|
||||
@@ -101,7 +126,11 @@ export class OverlayNotificationsContentService
|
||||
};
|
||||
|
||||
if (globalThis.document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => this.openNotificationBar(initData));
|
||||
globalThis.document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => this.openNotificationBar(initData),
|
||||
{ once: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,10 +144,13 @@ export class OverlayNotificationsContentService
|
||||
* @param message - The message containing the data for closing the notification bar.
|
||||
*/
|
||||
private handleCloseNotificationBarMessage(message: NotificationsExtensionMessage) {
|
||||
if (message.command !== "closeNotificationBar") {
|
||||
return;
|
||||
}
|
||||
const closedByUser =
|
||||
typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true;
|
||||
if (message.data?.fadeOutNotification) {
|
||||
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
|
||||
this.setContainerStyles({ opacity: "0" });
|
||||
globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150);
|
||||
return;
|
||||
}
|
||||
@@ -126,201 +158,524 @@ export class OverlayNotificationsContentService
|
||||
this.closeNotificationBar(closedByUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the height of the notification bar.
|
||||
*
|
||||
* @param message - The message containing the height of the notification bar.
|
||||
*/
|
||||
private handleAdjustNotificationBarHeightMessage(message: NotificationsExtensionMessage) {
|
||||
if (this.notificationBarElement && message.data?.height) {
|
||||
this.notificationBarElement.style.height = `${message.data.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the message for when a save cipher attempt has completed. This triggers an update
|
||||
* to the presentation of the notification bar, facilitating a visual indication of the save
|
||||
* attempt's success or failure.
|
||||
*
|
||||
* @param message
|
||||
* @private
|
||||
*/
|
||||
private handleSaveCipherAttemptCompletedMessage(message: NotificationsExtensionMessage) {
|
||||
// destructure error out of data
|
||||
const { error, ...otherData } = message?.data || {};
|
||||
|
||||
this.sendMessageToNotificationBarIframe({
|
||||
command: "saveCipherAttemptCompleted",
|
||||
data: Object.keys(otherData).length ? otherData : undefined,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the notification bar with the given initialization data.
|
||||
*
|
||||
* @param initData
|
||||
* @private
|
||||
*/
|
||||
private openNotificationBar(initData: NotificationBarIframeInitData) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
this.createNotificationBarIframeElement(initData);
|
||||
this.createNotificationBarElement();
|
||||
|
||||
this.setupInitNotificationBarMessageListener(initData);
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the iframe element for the notification bar.
|
||||
*
|
||||
* @param initData - The initialization data for the notification bar.
|
||||
*/
|
||||
private createNotificationBarIframeElement(initData: NotificationBarIframeInitData) {
|
||||
const isNotificationFresh =
|
||||
initData.launchTimestamp && Date.now() - initData.launchTimestamp < 250;
|
||||
|
||||
this.currentNotificationBarType = initData.type;
|
||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||
const parentOrigin = globalThis.location.origin;
|
||||
const iframeUrl = new URL(BrowserApi.getRuntimeURL("notification/bar.html"));
|
||||
iframeUrl.searchParams.set("parentOrigin", parentOrigin);
|
||||
this.notificationBarIframeElement.src = iframeUrl.toString();
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{
|
||||
...this.notificationBarIframeElementStyles,
|
||||
transform: isNotificationFresh ? "translateX(100%)" : "translateX(0)",
|
||||
opacity: isNotificationFresh ? "1" : "0",
|
||||
},
|
||||
true,
|
||||
);
|
||||
this.notificationBarIframeElement.addEventListener(
|
||||
EVENTS.LOAD,
|
||||
this.handleNotificationBarIframeOnLoad,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the load event for the notification bar iframe.
|
||||
* This will animate the notification bar into view.
|
||||
*/
|
||||
private handleNotificationBarIframeOnLoad = () => {
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
|
||||
this.notificationBarIframeElement?.removeEventListener(
|
||||
EVENTS.LOAD,
|
||||
this.handleNotificationBarIframeOnLoad,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container for the notification bar iframe with shadow DOM.
|
||||
*/
|
||||
private createNotificationBarElement() {
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarRootElement = globalThis.document.createElement(
|
||||
"bit-notification-bar-root",
|
||||
);
|
||||
|
||||
this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({
|
||||
mode: "closed",
|
||||
delegatesFocus: true,
|
||||
});
|
||||
|
||||
this.notificationBarElement = globalThis.document.createElement("div");
|
||||
this.notificationBarElement.id = "bit-notification-bar";
|
||||
|
||||
setElementStyles(this.notificationBarElement, this.notificationBarContainerStyles, true);
|
||||
|
||||
this.notificationBarShadowRoot.appendChild(this.notificationBarElement);
|
||||
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the message listener for the initialization of the notification bar.
|
||||
* This will send the initialization data to the notification bar iframe.
|
||||
*
|
||||
* @param initData - The initialization data for the notification bar.
|
||||
*/
|
||||
private setupInitNotificationBarMessageListener(initData: NotificationBarIframeInitData) {
|
||||
const handleInitNotificationBarMessage = (event: MessageEvent) => {
|
||||
const { source, data } = event;
|
||||
if (
|
||||
source !== this.notificationBarIframeElement.contentWindow ||
|
||||
data?.command !== "initNotificationBar"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMessageToNotificationBarIframe({
|
||||
command: "initNotificationBar",
|
||||
initData,
|
||||
parentOrigin: globalThis.location.origin,
|
||||
});
|
||||
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
||||
};
|
||||
|
||||
if (this.notificationBarIframeElement) {
|
||||
globalThis.addEventListener("message", handleInitNotificationBarMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the notification bar. Will trigger a removal of the notification bar
|
||||
* from the background queue if the notification bar was closed by the user.
|
||||
*
|
||||
* @param closedByUserAction - Whether the notification bar was closed by the user.
|
||||
*/
|
||||
private closeNotificationBar(closedByUserAction: boolean = false) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
if (message.command !== "saveCipherAttemptCompleted") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationBarIframeElement.remove();
|
||||
this.notificationBarIframeElement = null;
|
||||
const { error, cipherId, task, itemName } = message.data || {};
|
||||
const validatedTask = this.validateTaskInfo(task);
|
||||
this.renderSaveCipherConfirmation(error, { cipherId, task: validatedTask, itemName });
|
||||
}
|
||||
|
||||
this.notificationBarElement.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
private validateTaskInfo(
|
||||
task: NotificationTaskInfo | undefined,
|
||||
): NotificationTaskInfo | undefined {
|
||||
if (!task || typeof task !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const orgName = typeof task.orgName === "string" ? task.orgName : undefined;
|
||||
const remainingTasksCount =
|
||||
typeof task.remainingTasksCount === "number" &&
|
||||
Number.isFinite(task.remainingTasksCount) &&
|
||||
task.remainingTasksCount >= 0
|
||||
? task.remainingTasksCount
|
||||
: undefined;
|
||||
|
||||
if (orgName === undefined || remainingTasksCount === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { orgName, remainingTasksCount };
|
||||
}
|
||||
|
||||
private openNotificationBar(initData: NotificationBarIframeInitData) {
|
||||
if (
|
||||
!this.notificationBarRootElement ||
|
||||
!globalThis.document.body.contains(this.notificationBarRootElement)
|
||||
) {
|
||||
if (this.notificationBarRootElement) {
|
||||
this.closeNotificationBar();
|
||||
}
|
||||
this.createNotificationBarElement();
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
|
||||
this.currentNotificationBarType = initData.type as NotificationType;
|
||||
this.prepareNotificationBarEntrance(initData);
|
||||
void this.renderNotificationBarContent(initData);
|
||||
}
|
||||
|
||||
private async renderNotificationBarContent(initData: NotificationBarIframeInitData) {
|
||||
if (!this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentNotificationBarInitData = initData;
|
||||
const { i18n, resolvedTheme, notificationType, headerMessage, notificationTestId } =
|
||||
this.getNotificationConfig(initData);
|
||||
const personalVaultDisallowed = Boolean(initData.removeIndividualVault);
|
||||
|
||||
if (initData.isVaultLocked) {
|
||||
this.renderLockedNotification({
|
||||
initData,
|
||||
notificationType,
|
||||
headerMessage,
|
||||
notificationTestId,
|
||||
resolvedTheme,
|
||||
personalVaultDisallowed,
|
||||
i18n,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (initData.type === NotificationTypes.AtRiskPassword) {
|
||||
this.renderContent(
|
||||
AtRiskNotification({
|
||||
...initData,
|
||||
type: notificationType,
|
||||
theme: resolvedTheme,
|
||||
i18n,
|
||||
notificationTestId,
|
||||
params: initData.params,
|
||||
handleCloseNotification: this.handleCloseNotification,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const orgId = selectedVaultSignal.get();
|
||||
const [organizations, folders, ciphers, collections] = await Promise.all([
|
||||
sendExtensionMessage("bgGetOrgData"),
|
||||
sendExtensionMessage("bgGetFolderData"),
|
||||
sendExtensionMessage("bgGetDecryptedCiphers"),
|
||||
sendExtensionMessage("bgGetCollectionData", { orgId }),
|
||||
]);
|
||||
|
||||
this.currentNotificationBarInitData = {
|
||||
...initData,
|
||||
organizations,
|
||||
folders,
|
||||
ciphers,
|
||||
collections,
|
||||
};
|
||||
|
||||
this.renderContent(
|
||||
NotificationContainer({
|
||||
...this.currentNotificationBarInitData,
|
||||
headerMessage,
|
||||
type: notificationType,
|
||||
theme: resolvedTheme,
|
||||
notificationTestId,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification: this.handleCloseNotification,
|
||||
handleSaveAction: this.handleSaveAction,
|
||||
handleEditOrUpdateAction: this.handleEditOrUpdateAction,
|
||||
i18n,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private renderContent(template: Parameters<typeof render>[0]) {
|
||||
if (!this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
render(template, this.notificationBarElement);
|
||||
this.syncEmotionStyles();
|
||||
}
|
||||
|
||||
private getNotificationConfig(initData: NotificationBarIframeInitData) {
|
||||
const i18n = this.getI18n();
|
||||
const resolvedTheme = getResolvedTheme((initData.theme ?? ThemeTypes.Light) as Theme);
|
||||
const notificationType = resolveNotificationType(initData);
|
||||
const headerMessage = getNotificationHeaderMessage(i18n, notificationType);
|
||||
const notificationTestId = getNotificationTestId(notificationType);
|
||||
return { i18n, resolvedTheme, notificationType, headerMessage, notificationTestId };
|
||||
}
|
||||
|
||||
private renderLockedNotification({
|
||||
initData,
|
||||
notificationType,
|
||||
headerMessage,
|
||||
notificationTestId,
|
||||
resolvedTheme,
|
||||
personalVaultDisallowed,
|
||||
i18n,
|
||||
}: {
|
||||
initData: NotificationBarIframeInitData;
|
||||
notificationType: NotificationType;
|
||||
headerMessage?: string;
|
||||
notificationTestId: string;
|
||||
resolvedTheme: Theme;
|
||||
personalVaultDisallowed: boolean;
|
||||
i18n: I18n;
|
||||
}) {
|
||||
if (!this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationConfig = {
|
||||
...initData,
|
||||
headerMessage,
|
||||
type: notificationType,
|
||||
theme: resolvedTheme,
|
||||
notificationTestId,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification: this.handleCloseNotification,
|
||||
handleEditOrUpdateAction: this.handleEditOrUpdateAction,
|
||||
i18n,
|
||||
};
|
||||
|
||||
const handleSaveAction = () => {
|
||||
this.sendSaveCipherMessage(null, true);
|
||||
this.renderContent(
|
||||
NotificationContainer({
|
||||
...notificationConfig,
|
||||
handleSaveAction: () => {},
|
||||
isLoading: true,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
this.renderContent(
|
||||
NotificationContainer({
|
||||
...notificationConfig,
|
||||
handleSaveAction,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private renderSaveCipherConfirmation(
|
||||
error?: string,
|
||||
data?: { cipherId?: string; task?: NotificationTaskInfo; itemName?: string },
|
||||
) {
|
||||
if (!this.notificationBarElement || !this.currentNotificationBarInitData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { i18n, resolvedTheme } = this.getNotificationConfig(this.currentNotificationBarInitData);
|
||||
const resolvedType = resolveNotificationType(this.currentNotificationBarInitData);
|
||||
const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error);
|
||||
const notificationTestId = getNotificationTestId(resolvedType, true);
|
||||
|
||||
globalThis.setTimeout(() => {
|
||||
void sendExtensionMessage("bgCloseNotificationBar");
|
||||
}, 5000);
|
||||
|
||||
this.renderContent(
|
||||
NotificationConfirmationContainer({
|
||||
...this.currentNotificationBarInitData,
|
||||
error,
|
||||
headerMessage,
|
||||
theme: resolvedTheme,
|
||||
notificationTestId,
|
||||
task: data?.task,
|
||||
itemName: data?.itemName ?? i18n.typeLogin,
|
||||
handleCloseNotification: this.handleCloseNotification,
|
||||
handleOpenTasks: () => {
|
||||
void sendExtensionMessage("bgOpenAtRiskPasswords");
|
||||
},
|
||||
handleOpenVault: (event: Event) => {
|
||||
if (data?.cipherId) {
|
||||
this.openViewVaultItemPopout(data.cipherId);
|
||||
return;
|
||||
}
|
||||
this.openAddEditVaultItemPopout(event);
|
||||
},
|
||||
i18n,
|
||||
type: this.currentNotificationBarInitData.type as NotificationType,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private createNotificationBarElement() {
|
||||
this.notificationBarRootElement = globalThis.document.createElement(
|
||||
"bit-notification-bar-root",
|
||||
);
|
||||
this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({
|
||||
mode: "closed",
|
||||
delegatesFocus: true,
|
||||
});
|
||||
|
||||
this.notificationBarElement = globalThis.document.createElement("div");
|
||||
this.notificationBarElement.id = "bit-notification-bar";
|
||||
this.expectedContainerStyles = { ...notificationBarContainerStyles };
|
||||
this.updateElementStyles(this.notificationBarElement, this.expectedContainerStyles);
|
||||
this.notificationBarShadowRoot.appendChild(this.notificationBarElement);
|
||||
this.mutationObserver = new MutationObserver(this.handleMutations);
|
||||
this.observeNotificationBar();
|
||||
this.syncEmotionStyles();
|
||||
}
|
||||
|
||||
private syncEmotionStyles() {
|
||||
if (!this.notificationBarShadowRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingStyle = this.notificationBarShadowRoot.querySelector(
|
||||
"style[data-bit-notification-styles]",
|
||||
);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
const emotionStyles = globalThis.document.head.querySelectorAll(emotionStyleSelctor);
|
||||
const combinedCSS = Array.from(emotionStyles)
|
||||
.map((styleElement) => styleElement.textContent)
|
||||
.filter((text): text is string => Boolean(text))
|
||||
.join("\n");
|
||||
|
||||
if (combinedCSS) {
|
||||
const combinedStyle = globalThis.document.createElement("style");
|
||||
combinedStyle.setAttribute("data-bit-notification-styles", "true");
|
||||
combinedStyle.textContent = combinedCSS;
|
||||
this.notificationBarShadowRoot.prepend(combinedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private prepareNotificationBarEntrance(initData: NotificationBarIframeInitData) {
|
||||
const isFresh = Boolean(
|
||||
initData.launchTimestamp && Date.now() - initData.launchTimestamp < 250,
|
||||
);
|
||||
|
||||
if (isFresh) {
|
||||
this.setContainerStyles({ transform: "translateX(100%)", opacity: "1" });
|
||||
requestAnimationFrame(() => this.setContainerStyles({ transform: "translateX(0)" }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setContainerStyles({ transform: "translateX(0)", opacity: "1" });
|
||||
}
|
||||
|
||||
private setContainerStyles(styles: Partial<CSSStyleDeclaration>) {
|
||||
if (!this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.expectedContainerStyles = { ...this.expectedContainerStyles, ...styles };
|
||||
this.updateElementStyles(this.notificationBarElement, this.expectedContainerStyles);
|
||||
}
|
||||
|
||||
private updateElementStyles(element: HTMLElement, styles: Partial<CSSStyleDeclaration>) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unobserveNotificationBar();
|
||||
setElementStyles(element, styles, true);
|
||||
this.observeNotificationBar();
|
||||
}
|
||||
|
||||
private handleMutations = (mutations: MutationRecord[]) => {
|
||||
if (this.isTriggeringExcessiveMutationObserverIterations() || !this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "attributes") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const element = mutation.target as HTMLElement;
|
||||
if (mutation.attributeName === "style") {
|
||||
this.notificationBarElement.removeAttribute("style");
|
||||
this.updateElementStyles(this.notificationBarElement, this.expectedContainerStyles);
|
||||
} else {
|
||||
this.handleElementAttributeMutation(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleElementAttributeMutation(element: HTMLElement) {
|
||||
if (!this.notificationBarElement || this.foreignMutationsCount >= 10) {
|
||||
if (this.foreignMutationsCount >= 10) {
|
||||
this.forceCloseNotificationBar();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const attribute of Array.from(element.attributes)) {
|
||||
if (attribute.name === "style") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const expected = this.defaultNotificationBarAttributes[attribute.name];
|
||||
if (!expected) {
|
||||
this.notificationBarElement.removeAttribute(attribute.name);
|
||||
this.foreignMutationsCount++;
|
||||
} else if (attribute.value !== expected) {
|
||||
this.notificationBarElement.setAttribute(attribute.name, expected);
|
||||
this.foreignMutationsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private observeNotificationBar() {
|
||||
if (!this.mutationObserver || !this.notificationBarElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mutationObserver.observe(this.notificationBarElement, { attributes: true });
|
||||
}
|
||||
|
||||
private unobserveNotificationBar() {
|
||||
this.mutationObserver?.disconnect();
|
||||
}
|
||||
|
||||
private isTriggeringExcessiveMutationObserverIterations() {
|
||||
if (this.mutationObserverIterationsResetTimeout) {
|
||||
clearTimeout(this.mutationObserverIterationsResetTimeout);
|
||||
}
|
||||
|
||||
this.mutationObserverIterations++;
|
||||
this.mutationObserverIterationsResetTimeout = globalThis.setTimeout(() => {
|
||||
this.mutationObserverIterations = 0;
|
||||
this.foreignMutationsCount = 0;
|
||||
}, 2000);
|
||||
|
||||
if (this.mutationObserverIterations > 20) {
|
||||
clearTimeout(this.mutationObserverIterationsResetTimeout);
|
||||
this.mutationObserverIterations = 0;
|
||||
this.foreignMutationsCount = 0;
|
||||
this.forceCloseNotificationBar();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private forceCloseNotificationBar() {
|
||||
this.closeNotificationBar(true);
|
||||
}
|
||||
|
||||
private handleCloseNotification = (event?: Event) => {
|
||||
event?.preventDefault();
|
||||
void sendExtensionMessage("bgCloseNotificationBar", { fadeOutNotification: true });
|
||||
};
|
||||
|
||||
private handleEditOrUpdateAction = (event: Event) => {
|
||||
event.preventDefault();
|
||||
const currentType = this.currentNotificationBarInitData
|
||||
? resolveNotificationType(this.currentNotificationBarInitData)
|
||||
: NotificationTypes.Change;
|
||||
this.sendSaveCipherMessage(selectedCipherSignal.get(), currentType === NotificationTypes.Add);
|
||||
};
|
||||
|
||||
private handleSaveAction = (event: Event) => {
|
||||
const selectedCipher = selectedCipherSignal.get();
|
||||
const selectedVault = selectedVaultSignal.get();
|
||||
const selectedFolder = selectedFolderSignal.get();
|
||||
|
||||
if (selectedVault.length > 1) {
|
||||
this.openAddEditVaultItemPopout(event, {
|
||||
organizationId: selectedVault,
|
||||
...(selectedFolder?.length > 1 ? { folder: selectedFolder } : {}),
|
||||
});
|
||||
this.handleCloseNotification(event);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const disallowPersonalVault = Boolean(
|
||||
this.currentNotificationBarInitData?.removeIndividualVault,
|
||||
);
|
||||
this.sendSaveCipherMessage(selectedCipher, disallowPersonalVault, selectedFolder);
|
||||
if (disallowPersonalVault) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private sendSaveCipherMessage(cipherId: string | null, edit: boolean, folder?: string) {
|
||||
void sendExtensionMessage("bgSaveCipher", {
|
||||
cipherId,
|
||||
folder,
|
||||
edit,
|
||||
});
|
||||
}
|
||||
|
||||
private openAddEditVaultItemPopout(
|
||||
event: Event,
|
||||
options: { cipherId?: string; organizationId?: string; folder?: string } = {},
|
||||
) {
|
||||
event.preventDefault();
|
||||
void sendExtensionMessage("bgOpenAddEditVaultItemPopout", options);
|
||||
}
|
||||
|
||||
private openViewVaultItemPopout(cipherId: string) {
|
||||
void sendExtensionMessage("bgOpenViewVaultItemPopout", { cipherId });
|
||||
}
|
||||
|
||||
private closeNotificationBar(closedByUserAction: boolean = false) {
|
||||
if (!this.notificationBarRootElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removableNotificationTypes = new Set([
|
||||
NotificationTypes.Add,
|
||||
NotificationTypes.Change,
|
||||
NotificationTypes.AtRiskPassword,
|
||||
] as NotificationType[]);
|
||||
const shouldNotifyQueue =
|
||||
closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType);
|
||||
|
||||
if (closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType)) {
|
||||
this.unobserveNotificationBar();
|
||||
this.mutationObserver = null;
|
||||
|
||||
if (this.mutationObserverIterationsResetTimeout) {
|
||||
clearTimeout(this.mutationObserverIterationsResetTimeout);
|
||||
this.mutationObserverIterationsResetTimeout = null;
|
||||
}
|
||||
|
||||
this.notificationBarElement?.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
this.currentNotificationBarInitData = null;
|
||||
this.expectedContainerStyles = {};
|
||||
|
||||
if (shouldNotifyQueue) {
|
||||
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
||||
}
|
||||
|
||||
this.currentNotificationBarType = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the notification bar iframe.
|
||||
*
|
||||
* @param message - The message to send to the notification bar iframe.
|
||||
*/
|
||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the notification bar.
|
||||
*/
|
||||
destroy() {
|
||||
this.closeNotificationBar(true);
|
||||
}
|
||||
|
||||
private getI18n(): I18n {
|
||||
return {
|
||||
atRiskPassword: chrome.i18n.getMessage("atRiskPassword"),
|
||||
changePassword: chrome.i18n.getMessage("changePassword"),
|
||||
close: chrome.i18n.getMessage("close"),
|
||||
collection: chrome.i18n.getMessage("collection"),
|
||||
folder: chrome.i18n.getMessage("folder"),
|
||||
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
|
||||
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
|
||||
myVault: chrome.i18n.getMessage("myVault"),
|
||||
newItem: chrome.i18n.getMessage("newItem"),
|
||||
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
|
||||
notificationLoginSaveConfirmation: chrome.i18n.getMessage(
|
||||
"notificationLoginSaveConfirmation",
|
||||
),
|
||||
notificationLoginUpdatedConfirmation: chrome.i18n.getMessage(
|
||||
"notificationLoginUpdatedConfirmation",
|
||||
),
|
||||
notificationNewItemAria: chrome.i18n.getMessage("notificationNewItemAria"),
|
||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
|
||||
saveAction: chrome.i18n.getMessage("notificationAddSave"),
|
||||
saveFailure: chrome.i18n.getMessage("saveFailure"),
|
||||
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
|
||||
saveLogin: chrome.i18n.getMessage("saveLogin"),
|
||||
typeLogin: chrome.i18n.getMessage("typeLogin"),
|
||||
unlockToSave: chrome.i18n.getMessage("unlockToSave"),
|
||||
updateLogin: chrome.i18n.getMessage("updateLogin"),
|
||||
vault: chrome.i18n.getMessage("vault"),
|
||||
view: chrome.i18n.getMessage("view"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"content/fido2-page-script.js",
|
||||
"notification/bar.html",
|
||||
"images/icon38.png",
|
||||
"images/icon38_locked.png",
|
||||
"overlay/menu-button.html",
|
||||
|
||||
@@ -156,7 +156,6 @@
|
||||
{
|
||||
"resources": [
|
||||
"content/fido2-page-script.js",
|
||||
"notification/bar.html",
|
||||
"images/icon38.png",
|
||||
"images/icon38_locked.png",
|
||||
"overlay/menu-button.html",
|
||||
|
||||
@@ -145,11 +145,6 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
chunks: ["popup/polyfills", "popup/vendor-angular", "popup/vendor", "popup/main"],
|
||||
browser: browser,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, "src/autofill/notification/bar.html"),
|
||||
filename: "notification/bar.html",
|
||||
chunks: ["notification/bar"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(
|
||||
__dirname,
|
||||
@@ -264,7 +259,6 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
__dirname,
|
||||
"src/platform/ipc/content/ipc-content-script.ts",
|
||||
),
|
||||
"notification/bar": path.resolve(__dirname, "src/autofill/notification/bar.ts"),
|
||||
"overlay/menu-button": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
"../../apps/browser/src/autofill/content/*.ts",
|
||||
"../../apps/browser/src/autofill/fido2/content/*.ts",
|
||||
"../../apps/browser/src/autofill/notification/bar.ts",
|
||||
"../../apps/browser/src/autofill/notification/notification-bar-helpers.ts",
|
||||
"../../apps/browser/src/autofill/overlay/inline-menu/**/*.ts",
|
||||
"../../apps/browser/src/platform/ipc/content/*.ts",
|
||||
"../../apps/browser/src/platform/offscreen-document/offscreen-document.ts",
|
||||
|
||||
Reference in New Issue
Block a user