From abff1d3824f0ba3d24961fd897d5ba940d421817 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 8 Dec 2025 17:10:43 -0500 Subject: [PATCH] =?UTF-8?q?PM-27310:=20WIP=20=E2=80=93=20rough=20proof=20o?= =?UTF-8?q?f=20concept=20to=20replace=20iframe=20based=20notification=20ba?= =?UTF-8?q?r=20with=20Shadow=20DOM=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../container.lit-stories.ts | 2 +- .../confirmation/container.lit-stories.ts | 2 +- .../notification/container.lit-stories.ts | 5 +- .../option-selection/option-selection.ts | 4 +- .../src/autofill/notification/bar.html | 8 - .../src/autofill/notification/bar.spec.ts | 121 --- apps/browser/src/autofill/notification/bar.ts | 457 ---------- .../notification/notification-bar-helpers.ts | 64 ++ .../overlay-notifications-content.service.ts | 42 +- ...notifications-content.service.spec.ts.snap | 14 - .../overlay-notifications-content.service.ts | 783 +++++++++++++----- apps/browser/src/manifest.json | 1 - apps/browser/src/manifest.v3.json | 1 - apps/browser/webpack.base.js | 6 - bitwarden_license/bit-browser/tsconfig.json | 2 +- 15 files changed, 674 insertions(+), 838 deletions(-) delete mode 100644 apps/browser/src/autofill/notification/bar.html delete mode 100644 apps/browser/src/autofill/notification/bar.spec.ts delete mode 100644 apps/browser/src/autofill/notification/bar.ts create mode 100644 apps/browser/src/autofill/notification/notification-bar-helpers.ts delete mode 100644 apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/at-risk-notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/at-risk-notification/container.lit-stories.ts index 55d35869db4..d0d29bd7dd8 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/at-risk-notification/container.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/at-risk-notification/container.lit-stories.ts @@ -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, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts index 55c0a456732..7d99bfa0d58 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts @@ -6,7 +6,7 @@ import { NotificationTypes } from "../../../../../notification/abstractions/noti import { getConfirmationHeaderMessage, getNotificationTestId, -} from "../../../../../notification/bar"; +} from "../../../../../notification/notification-bar-helpers"; import { NotificationConfirmationContainer, NotificationConfirmationContainerProps, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts index d09bc76c834..143ad2daf7b 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts @@ -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"; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index ee711456e9c..a5d6b6e43b6 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -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]; diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html deleted file mode 100644 index 8934fe6a031..00000000000 --- a/apps/browser/src/autofill/notification/bar.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - Bitwarden - - - - diff --git a/apps/browser/src/autofill/notification/bar.spec.ts b/apps/browser/src/autofill/notification/bar.spec.ts deleted file mode 100644 index ae60e2efc91..00000000000 --- a/apps/browser/src/autofill/notification/bar.spec.ts +++ /dev/null @@ -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 => ({ - command: "initNotificationBar", - ...overrides, - }); - - beforeEach(() => { - Object.defineProperty(globalThis, "location", { - value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` }, - writable: true, - configurable: true, - }); - Object.defineProperty(globalThis, "parent", { - value: mock(), - 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(), - }, - { - 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(); - }); -}); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts deleted file mode 100644 index 333f8d5e534..00000000000 --- a/apps/browser/src/autofill/notification/bar.ts +++ /dev/null @@ -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((resolve) => sendPlatformMessage({ command: "bgGetOrgData" }, resolve)), - new Promise((resolve) => - sendPlatformMessage({ command: "bgGetFolderData" }, resolve), - ), - new Promise((resolve) => - sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve), - ), - new Promise((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, - 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); -} diff --git a/apps/browser/src/autofill/notification/notification-bar-helpers.ts b/apps/browser/src/autofill/notification/notification-bar-helpers.ts new file mode 100644 index 00000000000..4330b76e54a --- /dev/null +++ b/apps/browser/src/autofill/notification/notification-bar-helpers.ts @@ -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; +} diff --git a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts index 338c79cc607..4a8ea7bed6b 100644 --- a/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/abstractions/overlay-notifications-content.service.ts @@ -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; }; diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap deleted file mode 100644 index cfcedc9da7a..00000000000 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ /dev/null @@ -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`] = ` -
-