1
0
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:
Daniel Riera
2025-12-08 17:10:43 -05:00
parent 17ebae11d7
commit abff1d3824
15 changed files with 674 additions and 838 deletions

View File

@@ -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,

View File

@@ -6,7 +6,7 @@ import { NotificationTypes } from "../../../../../notification/abstractions/noti
import {
getConfirmationHeaderMessage,
getNotificationTestId,
} from "../../../../../notification/bar";
} from "../../../../../notification/notification-bar-helpers";
import {
NotificationConfirmationContainer,
NotificationConfirmationContainerProps,

View File

@@ -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";

View File

@@ -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];

View File

@@ -1,8 +0,0 @@
<!doctype html>
<html>
<head>
<title>Bitwarden</title>
<meta charset="utf-8" />
</head>
<body></body>
</html>

View File

@@ -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();
});
});

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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>
`;

View File

@@ -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"),
};
}
}

View File

@@ -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",

View File

@@ -156,7 +156,6 @@
{
"resources": [
"content/fido2-page-script.js",
"notification/bar.html",
"images/icon38.png",
"images/icon38_locked.png",
"overlay/menu-button.html",

View File

@@ -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",

View File

@@ -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",