diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts index 9068bbfc27d..a316d8f5baa 100644 --- a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts +++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts @@ -58,6 +58,10 @@ const config: StorybookConfig = { }, ], }); + config.module.rules.push({ + test: /\.scss$/, + use: [require.resolve("css-loader"), require.resolve("sass-loader")], + }); } return config; }, 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 477a50e25d2..b55903ba274 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 @@ -3,6 +3,7 @@ import { Meta, StoryObj } from "@storybook/web-components"; import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; +import { getConfirmationHeaderMessage } from "../../../../../notification/bar"; import { NotificationConfirmationContainer, NotificationConfirmationContainerProps, @@ -35,8 +36,10 @@ export default { }, } as Meta; -const Template = (args: NotificationConfirmationContainerProps) => - NotificationConfirmationContainer({ ...args }); +const Template = (args: NotificationConfirmationContainerProps) => { + const headerMessage = getConfirmationHeaderMessage(args.i18n, args.type, args.error); + return NotificationConfirmationContainer({ ...args, headerMessage }); +}; export const Default: StoryObj = { render: Template, 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 6c5664f0bc7..d086fe8b75a 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,6 +5,7 @@ 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 } from "../../../../notification/bar"; import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; import { mockBrowserI18nGetMessage, mockI18n } from "../mock-data"; @@ -46,7 +47,10 @@ export default { }, } as Meta; -const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args }); +const Template = (args: NotificationContainerProps) => { + const headerMessage = getNotificationHeaderMessage(args.i18n, args.type); + return NotificationContainer({ ...args, headerMessage }); +}; export const Default: StoryObj = { render: Template, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts index 611b0834765..ecc56bd5bd9 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -4,6 +4,7 @@ import { html } from "lit"; import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; import { NotificationHeader, NotificationHeaderProps } from "../../notification/header"; +import { mockI18n } from "../mock-data"; export default { title: "Components/Notifications/Header", @@ -17,6 +18,7 @@ export default { standalone: true, theme: ThemeTypes.Light, handleCloseNotification: () => alert("Close Clicked"), + i18n: mockI18n, }, parameters: { design: { diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts index dabb21e7d17..81ddb512201 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -25,6 +25,7 @@ export type NotificationConfirmationContainerProps = NotificationBarIframeInitDa handleOpenTasks: (e: Event) => void; } & { error?: string; + headerMessage?: string; i18n: I18n; itemName: string; task?: NotificationTaskInfo; @@ -36,13 +37,13 @@ export function NotificationConfirmationContainer({ handleCloseNotification, handleOpenVault, handleOpenTasks, + headerMessage, i18n, itemName, task, theme = ThemeTypes.Light, type, }: NotificationConfirmationContainerProps) { - const headerMessage = getHeaderMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, type, error); const buttonText = error ? i18n.newItem : i18n.view; const buttonAria = error @@ -125,20 +126,3 @@ function getConfirmationMessage(i18n: I18n, type?: NotificationType, error?: str ? i18n.notificationLoginSaveConfirmation : i18n.notificationLoginUpdatedConfirmation; } - -function getHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) { - if (error) { - return i18n.saveFailure; - } - - switch (type) { - case NotificationTypes.Add: - return i18n.loginSaveSuccess; - case NotificationTypes.Change: - return i18n.loginUpdateSuccess; - case NotificationTypes.Unlock: - return ""; - default: - return undefined; - } -} diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 313e3eecf01..b02f7dff6d0 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -27,6 +27,7 @@ export type NotificationContainerProps = NotificationBarIframeInitData & { ciphers?: NotificationCipherData[]; collections?: CollectionView[]; folders?: FolderView[]; + headerMessage?: string; i18n: I18n; organizations?: OrgView[]; personalVaultIsAllowed?: boolean; @@ -40,13 +41,13 @@ export function NotificationContainer({ ciphers, collections, folders, + headerMessage, i18n, organizations, personalVaultIsAllowed = true, theme = ThemeTypes.Light, type, }: NotificationContainerProps) { - const headerMessage = getHeaderMessage(i18n, type); const showBody = type !== NotificationTypes.Unlock; return html` @@ -98,16 +99,3 @@ const notificationContainerStyles = (theme: Theme) => css` padding-right: ${spacing["3"]}; } `; - -function getHeaderMessage(i18n: I18n, type?: NotificationType) { - switch (type) { - case NotificationTypes.Add: - return i18n.saveLogin; - case NotificationTypes.Change: - return i18n.updateLogin; - case NotificationTypes.Unlock: - return i18n.unlockToSave; - default: - return undefined; - } -} diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 50730f7ccaf..2b32fb1c673 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -6,7 +6,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { NotificationCipherData } from "../content/components/cipher/types"; -import { CollectionView, OrgView } from "../content/components/common-types"; +import { CollectionView, I18n, OrgView } from "../content/components/common-types"; import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder"; @@ -113,6 +113,68 @@ const findElementById = ( return element as ElementType; }; +/** + * 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, + }[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]: "", + }[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; +} + /** * Sets the text content of an element identified by ID within a template's content. * @@ -148,6 +210,10 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); if (useComponentBar) { + const resolvedType = resolveNotificationType(notificationBarIframeInitData); + const headerMessage = getNotificationHeaderMessage(i18n, resolvedType); + appendHeaderMessageToTitle(headerMessage); + 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()); @@ -156,7 +222,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { return render( NotificationContainer({ ...notificationBarIframeInitData, - type: NotificationTypes.Unlock, + headerMessage, + type: resolvedType, theme: resolvedTheme, personalVaultIsAllowed: !personalVaultDisallowed, handleCloseNotification, @@ -199,7 +266,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { return render( NotificationContainer({ ...notificationBarIframeInitData, - type: notificationBarIframeInitData.type as NotificationType, + headerMessage, + type: resolvedType, theme: resolvedTheme, personalVaultIsAllowed: !personalVaultDisallowed, handleCloseNotification, @@ -429,6 +497,8 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { const { cipherId, task, itemName } = data || {}; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); + const resolvedType = resolveNotificationType(notificationBarIframeInitData); + const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error); globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000); @@ -438,6 +508,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { type: type as NotificationType, theme: resolvedTheme, handleCloseNotification, + headerMessage, i18n, error, itemName: itemName ?? i18n.typeLogin,