diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation-container.ts
new file mode 100644
index 00000000000..4fda95986db
--- /dev/null
+++ b/apps/browser/src/autofill/content/components/notification/confirmation-container.ts
@@ -0,0 +1,98 @@
+import { css } from "@emotion/css";
+import { html } from "lit";
+
+import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
+
+import {
+ NotificationBarIframeInitData,
+ NotificationTypes,
+ NotificationType,
+} from "../../../notification/abstractions/notification-bar";
+import { themes, spacing } from "../constants/styles";
+
+import { NotificationConfirmationBody } from "./confirmation";
+import {
+ NotificationHeader,
+ componentClassPrefix as notificationHeaderClassPrefix,
+} from "./header";
+
+export function NotificationConfirmationContainer({
+ error,
+ handleCloseNotification,
+ i18n,
+ theme = ThemeTypes.Light,
+ type,
+}: NotificationBarIframeInitData & {
+ handleCloseNotification: (e: Event) => void;
+} & {
+ error: string;
+ i18n: { [key: string]: string };
+ type: NotificationType;
+}) {
+ const headerMessage = getHeaderMessage(i18n, type, error);
+ const confirmationMessage = getConfirmationMessage(i18n, type, error);
+ const buttonText = error ? i18n.newItem : i18n.view;
+
+ return html`
+
+ ${NotificationHeader({
+ handleCloseNotification,
+ message: headerMessage,
+ theme,
+ })}
+ ${NotificationConfirmationBody({
+ error: error,
+ buttonText,
+ confirmationMessage,
+ theme,
+ })}
+
+ `;
+}
+
+const notificationContainerStyles = (theme: Theme) => css`
+ position: absolute;
+ right: 20px;
+ border: 1px solid ${themes[theme].secondary["300"]};
+ border-radius: ${spacing["4"]};
+ box-shadow: -2px 4px 6px 0px #0000001a;
+ background-color: ${themes[theme].background.alt};
+ width: 400px;
+ overflow: hidden;
+
+ [class*="${notificationHeaderClassPrefix}-"] {
+ border-radius: ${spacing["4"]} ${spacing["4"]} 0 0;
+ border-bottom: 0.5px solid ${themes[theme].secondary["300"]};
+ }
+`;
+
+function getConfirmationMessage(
+ i18n: { [key: string]: string },
+ type?: NotificationType,
+ error?: string,
+) {
+ if (error) {
+ return i18n.saveFailureDetails;
+ }
+ return type === "add" ? i18n.loginSaveSuccessDetails : i18n.loginUpdateSuccessDetails;
+}
+function getHeaderMessage(
+ i18n: { [key: string]: string },
+ 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/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts
index 85f6e48cd5d..50c2c629942 100644
--- a/apps/browser/src/autofill/content/components/notification/header.ts
+++ b/apps/browser/src/autofill/content/components/notification/header.ts
@@ -17,12 +17,12 @@ const { css } = createEmotion({
export function NotificationHeader({
message,
- standalone,
+ standalone = false,
theme = ThemeTypes.Light,
handleCloseNotification,
}: {
message?: string;
- standalone: boolean;
+ standalone?: boolean;
theme: Theme;
handleCloseNotification: (e: Event) => void;
}) {
@@ -49,7 +49,7 @@ const notificationHeaderStyles = ({
display: flex;
align-items: center;
justify-content: flex-start;
- background-color: ${themes[theme].background.alt};
+ background-color: ${themes[theme].background};
padding: 12px 16px 8px 16px;
white-space: nowrap;
diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts
index 5a8d7855bea..8c4c8a3e229 100644
--- a/apps/browser/src/autofill/notification/bar.ts
+++ b/apps/browser/src/autofill/notification/bar.ts
@@ -7,6 +7,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
+import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
import { NotificationContainer } from "../content/components/notification/container";
import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons";
@@ -22,12 +23,17 @@ 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),
+ saveCipherAttemptCompleted: ({ message }) =>
+ useComponentBar
+ ? handleSaveCipherConfirmation(message)
+ : handleSaveCipherAttemptCompletedMessage(message),
};
globalThis.addEventListener("load", load);
+
function load() {
setupWindowMessageListener();
sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => {
@@ -35,7 +41,6 @@ function load() {
applyNotificationBarStyle();
});
}
-
function applyNotificationBarStyle() {
if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -44,16 +49,8 @@ function applyNotificationBarStyle() {
postMessageToParent({ command: "initNotificationBar" });
}
-function initNotificationBar(message: NotificationBarWindowMessage) {
- const { initData } = message;
- if (!initData) {
- return;
- }
-
- notificationBarIframeInitData = initData;
- const { isVaultLocked, theme } = notificationBarIframeInitData;
-
- const i18n = {
+function getI18n() {
+ return {
appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"),
@@ -74,20 +71,30 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
updateLoginPrompt: "Update existing login?",
loginSaveSuccess: "Login saved",
loginSaveSuccessDetails: "Login saved to Bitwarden.",
- loginUpdateSuccess: "Login saved",
+ loginUpdateSuccess: "Login updated",
loginUpdateSuccessDetails: "Login updated in Bitwarden.",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item",
+ newItem: "New item",
+ view: "View",
};
+}
+
+function initNotificationBar(message: NotificationBarWindowMessage) {
+ const { initData } = message;
+ if (!initData) {
+ return;
+ }
+
+ notificationBarIframeInitData = initData;
+ const { isVaultLocked, theme } = notificationBarIframeInitData;
+ const i18n = getI18n();
+ const resolvedTheme = getResolvedTheme(theme);
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;
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
// @TODO use context to avoid prop drilling
@@ -105,77 +112,71 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body,
);
});
- }
+ } else {
+ setNotificationBarTheme();
- setNotificationBarTheme();
+ (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
+ ? chrome.runtime.getURL("images/icon38_locked.png")
+ : chrome.runtime.getURL("images/icon38.png");
- (document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
- ? chrome.runtime.getURL("images/icon38_locked.png")
- : chrome.runtime.getURL("images/icon38.png");
+ setupLogoLink(i18n);
- setupLogoLink(i18n);
+ // i18n for "Add" template
+ const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
- // i18n for "Add" template
- const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
+ const neverButton = addTemplate.content.getElementById("never-save");
+ neverButton.textContent = i18n.never;
- const neverButton = addTemplate.content.getElementById("never-save");
- neverButton.textContent = i18n.never;
+ const selectFolder = addTemplate.content.getElementById("select-folder");
+ selectFolder.hidden = isVaultLocked || removeIndividualVault();
+ selectFolder.setAttribute("aria-label", i18n.folder);
- const selectFolder = addTemplate.content.getElementById("select-folder");
- selectFolder.hidden = isVaultLocked || removeIndividualVault();
- selectFolder.setAttribute("aria-label", i18n.folder);
+ const addButton = addTemplate.content.getElementById("add-save");
+ addButton.textContent = i18n.notificationAddSave;
- const addButton = addTemplate.content.getElementById("add-save");
- addButton.textContent = i18n.notificationAddSave;
+ const addEditButton = addTemplate.content.getElementById("add-edit");
+ // If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
+ addEditButton.hidden = removeIndividualVault();
+ addEditButton.textContent = i18n.notificationEdit;
- const addEditButton = addTemplate.content.getElementById("add-edit");
- // If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
- addEditButton.hidden = removeIndividualVault();
- addEditButton.textContent = i18n.notificationEdit;
+ addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
- addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
+ // i18n for "Change" (update password) template
+ const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
- // i18n for "Change" (update password) template
- const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
+ const changeButton = changeTemplate.content.getElementById("change-save");
+ changeButton.textContent = i18n.notificationChangeSave;
- const changeButton = changeTemplate.content.getElementById("change-save");
- changeButton.textContent = i18n.notificationChangeSave;
+ const changeEditButton = changeTemplate.content.getElementById("change-edit");
+ changeEditButton.textContent = i18n.notificationEdit;
- const changeEditButton = changeTemplate.content.getElementById("change-edit");
- changeEditButton.textContent = i18n.notificationEdit;
+ changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
- changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
+ // i18n for "Unlock" (unlock extension) template
+ const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
- // i18n for "Unlock" (unlock extension) template
- const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
+ const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
+ unlockButton.textContent = i18n.notificationUnlock;
- const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
- unlockButton.textContent = i18n.notificationUnlock;
+ unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
- unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
+ // i18n for body content
+ const closeButton = document.getElementById("close-button");
+ closeButton.title = i18n.close;
- // i18n for body content
- const closeButton = document.getElementById("close-button");
- closeButton.title = i18n.close;
+ const notificationType = initData.type;
+ if (notificationType === "add") {
+ handleTypeAdd();
+ } else if (notificationType === "change") {
+ handleTypeChange();
+ } else if (notificationType === "unlock") {
+ handleTypeUnlock();
+ }
- const notificationType = initData.type;
- if (notificationType === "add") {
- handleTypeAdd();
- } else if (notificationType === "change") {
- handleTypeChange();
- } else if (notificationType === "unlock") {
- handleTypeUnlock();
- }
+ closeButton.addEventListener("click", handleCloseNotification);
- closeButton.addEventListener("click", handleCloseNotification);
-
- globalThis.addEventListener("resize", adjustHeight);
- adjustHeight();
- function handleCloseNotification(e: Event) {
- e.preventDefault();
- sendPlatformMessage({
- command: "bgCloseNotificationBar",
- });
+ globalThis.addEventListener("resize", adjustHeight);
+ adjustHeight();
}
function handleEditOrUpdateAction(e: Event) {
const notificationType = initData.type;
@@ -183,6 +184,12 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
}
}
+function handleCloseNotification(e: Event) {
+ e.preventDefault();
+ sendPlatformMessage({
+ command: "bgCloseNotificationBar",
+ });
+}
function handleSaveAction(e: Event) {
e.preventDefault();
@@ -282,6 +289,27 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
);
}
+function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
+ const { theme, type } = notificationBarIframeInitData;
+ const { error } = message;
+ const i18n = getI18n();
+ const resolvedTheme = getResolvedTheme(theme);
+
+ globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
+
+ return render(
+ NotificationConfirmationContainer({
+ ...notificationBarIframeInitData,
+ type: type as NotificationType,
+ theme: resolvedTheme,
+ handleCloseNotification,
+ i18n,
+ error,
+ }),
+ document.body,
+ );
+}
+
function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
@@ -395,6 +423,14 @@ function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]
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 setNotificationBarTheme() {
const theme = getTheme(globalThis, notificationBarIframeInitData.theme);