diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0175b27bd69..ad3bee97d8a 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -14,6 +14,7 @@ import { } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; @@ -80,6 +81,7 @@ export default class NotificationBackground { bgGetExcludedDomains: () => this.getExcludedDomains(), bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), + notificationRefreshFlagValue: () => this.getNotificationFlag(), }; private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); @@ -137,6 +139,15 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + /** + * Gets the current value of the notification refresh feature flag + * @returns Promise indicating if the feature is enabled + */ + async getNotificationFlag(): Promise { + const flagValue = await this.configService.getFeatureFlag(FeatureFlag.NotificationRefresh); + return flagValue; + } + private async getAuthStatus() { return await firstValueFrom(this.authService.activeAccountStatus$); } diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index d3e9c29ab58..8c6251b7030 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -852,10 +852,10 @@ async function loadNotificationBar() { } closeBar(false); - openBar(type, notificationBarUrl, notificationBarInitData); + void openBar(type, notificationBarUrl, notificationBarInitData); } - function openBar( + async function openBar( type: string, barPage: string, notificationBarInitData: NotificationBarIframeInitData, @@ -865,22 +865,38 @@ async function loadNotificationBar() { if (document.body == null) { return; } + const useComponentBar: boolean = await sendExtensionMessage("notificationRefreshFlagValue"); setupInitNotificationBarMessageListener(notificationBarInitData); const barPageUrl: string = chrome.runtime.getURL(barPage); + function getIframeStyle(useComponentBar: boolean): string { + return ( + (useComponentBar + ? "height: calc(276px + 25px); width: 450px; right: 0;" + : "height: 42px; width: 100%;") + " border: 0; min-height: initial;" + ); + } + notificationBarIframe = document.createElement("iframe"); - notificationBarIframe.style.cssText = - "height: 42px; width: 100%; border: 0; min-height: initial;"; + notificationBarIframe.style.cssText = getIframeStyle(useComponentBar); notificationBarIframe.id = "bit-notification-bar-iframe"; notificationBarIframe.src = barPageUrl; + function getFrameStyle(useComponentBar: boolean): string { + return ( + (useComponentBar + ? "height: calc(276px + 25px); width: 450px; right: 0;" + : "height: 42px; width: 100%; left: 0;") + + " top: 0; padding: 0; position: fixed;" + + " z-index: 2147483647; visibility: visible;" + ); + } + const frameDiv = document.createElement("div"); frameDiv.setAttribute("aria-live", "polite"); frameDiv.id = "bit-notification-bar"; - frameDiv.style.cssText = - "height: 42px; width: 100%; top: 0; left: 0; padding: 0; position: fixed; " + - "z-index: 2147483647; visibility: visible;"; + frameDiv.style.cssText = getFrameStyle(useComponentBar); frameDiv.appendChild(notificationBarIframe); document.body.appendChild(frameDiv); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 3fc61c448fe..202b144258d 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -1,10 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { render } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; +import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -12,15 +15,13 @@ import { NotificationBarWindowMessageHandlers, NotificationBarWindowMessage, NotificationBarIframeInitData, + NotificationType, } from "./abstractions/notification-bar"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./bar.scss"); - const logService = new ConsoleLogService(false); let notificationBarIframeInitData: NotificationBarIframeInitData = {}; let windowMessageOrigin: string; +let useComponentBar = false; const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = { initNotificationBar: ({ message }) => initNotificationBar(message), saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message), @@ -29,6 +30,17 @@ const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers globalThis.addEventListener("load", load); function load() { setupWindowMessageListener(); + sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => { + useComponentBar = flagValue; + applyNotificationBarStyle(); + }); +} + +function applyNotificationBarStyle() { + if (!useComponentBar) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + require("./bar.scss"); + } postMessageToParent({ command: "initNotificationBar" }); } @@ -39,12 +51,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) { } notificationBarIframeInitData = initData; - const { isVaultLocked } = notificationBarIframeInitData; - setNotificationBarTheme(); - - (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked - ? chrome.runtime.getURL("images/icon38_locked.png") - : chrome.runtime.getURL("images/icon38.png"); + const { isVaultLocked, theme } = notificationBarIframeInitData; const i18n = { appName: chrome.i18n.getMessage("appName"), @@ -58,8 +65,50 @@ function initNotificationBar(message: NotificationBarWindowMessage) { notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), + + // @TODO move values to message catalog + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + updateLoginAction: "Update login", + saveLoginPrompt: "Save login?", + updateLoginPrompt: "Update existing login?", + loginSaveSuccess: "Login saved", + loginSaveSuccessDetails: "Login saved to Bitwarden.", + loginUpdateSuccess: "Login saved", + loginUpdateSuccessDetails: "Login updated in Bitwarden.", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item", }; + if (useComponentBar) { + document.body.innerHTML = ""; + // Current implementations utilize a require for scss files which creates the need to remove the node. + document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove()); + + 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; + + // @TODO use context to avoid prop drilling + return render( + NotificationContainer({ + ...notificationBarIframeInitData, + type: notificationBarIframeInitData.type as NotificationType, + theme: resolvedTheme, + handleCloseNotification, + i18n, + }), + document.body, + ); + } + + setNotificationBarTheme(); + + (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked + ? chrome.runtime.getURL("images/icon38_locked.png") + : chrome.runtime.getURL("images/icon38.png"); + setupLogoLink(i18n); // i18n for "Add" template @@ -106,7 +155,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) { closeButton.title = i18n.close; const notificationType = initData.type; - if (initData.type === "add") { + if (notificationType === "add") { handleTypeAdd(); } else if (notificationType === "change") { handleTypeChange(); @@ -114,17 +163,19 @@ function initNotificationBar(message: NotificationBarWindowMessage) { handleTypeUnlock(); } - closeButton.addEventListener("click", (e) => { - e.preventDefault(); - sendPlatformMessage({ - command: "bgCloseNotificationBar", - }); - }); + closeButton.addEventListener("click", handleCloseNotification); globalThis.addEventListener("resize", adjustHeight); adjustHeight(); } +function handleCloseNotification(e: Event) { + e.preventDefault(); + sendPlatformMessage({ + command: "bgCloseNotificationBar", + }); +} + function handleTypeAdd() { setContent(document.getElementById("template-add") as HTMLTemplateElement); @@ -317,14 +368,19 @@ function setupLogoLink(i18n: Record) { sendPlatformMessage({ command: "getWebVaultUrlForNotification" }, setWebVaultUrlLink); } -function setNotificationBarTheme() { - let theme = notificationBarIframeInitData.theme; +function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]) { if (theme === ThemeTypes.System) { - theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches + return globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeTypes.Dark : ThemeTypes.Light; } + return theme; +} + +function setNotificationBarTheme() { + const theme = getTheme(globalThis, notificationBarIframeInitData.theme); + document.documentElement.classList.add(`theme_${theme}`); if (notificationBarIframeInitData.applyRedesign) {