From 9aee5f16c40b0cbde4e1a67bf50976818ff60ae1 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 26 Feb 2025 16:13:27 -0500 Subject: [PATCH] PM-18276-Connect confirmation UI (#13498) * PM-18276-wip * update typing * dynamically retrieve messages, resolve theme in function * five second timeout after save or update * adjust timeout to five seconds * negligible performance gain-revert * sacrifice contorl for to remove event listeners-revert --- .../notification/confirmation-container.ts | 98 ++++++++++ .../content/components/notification/header.ts | 6 +- apps/browser/src/autofill/notification/bar.ts | 174 +++++++++++------- 3 files changed, 206 insertions(+), 72 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-container.ts 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);