1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

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
This commit is contained in:
Daniel Riera
2025-02-26 16:13:27 -05:00
committed by GitHub
parent d999d91f19
commit 9aee5f16c4
3 changed files with 206 additions and 72 deletions

View File

@@ -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`
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
message: headerMessage,
theme,
})}
${NotificationConfirmationBody({
error: error,
buttonText,
confirmationMessage,
theme,
})}
</div>
`;
}
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;
}
}

View File

@@ -17,12 +17,12 @@ const { css } = createEmotion({
export function NotificationHeader({ export function NotificationHeader({
message, message,
standalone, standalone = false,
theme = ThemeTypes.Light, theme = ThemeTypes.Light,
handleCloseNotification, handleCloseNotification,
}: { }: {
message?: string; message?: string;
standalone: boolean; standalone?: boolean;
theme: Theme; theme: Theme;
handleCloseNotification: (e: Event) => void; handleCloseNotification: (e: Event) => void;
}) { }) {
@@ -49,7 +49,7 @@ const notificationHeaderStyles = ({
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
background-color: ${themes[theme].background.alt}; background-color: ${themes[theme].background};
padding: 12px 16px 8px 16px; padding: 12px 16px 8px 16px;
white-space: nowrap; white-space: nowrap;

View File

@@ -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 type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
import { NotificationContainer } from "../content/components/notification/container"; import { NotificationContainer } from "../content/components/notification/container";
import { buildSvgDomElement } from "../utils"; import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons"; import { circleCheckIcon } from "../utils/svg-icons";
@@ -22,12 +23,17 @@ const logService = new ConsoleLogService(false);
let notificationBarIframeInitData: NotificationBarIframeInitData = {}; let notificationBarIframeInitData: NotificationBarIframeInitData = {};
let windowMessageOrigin: string; let windowMessageOrigin: string;
let useComponentBar = false; let useComponentBar = false;
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = { const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
initNotificationBar: ({ message }) => initNotificationBar(message), initNotificationBar: ({ message }) => initNotificationBar(message),
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message), saveCipherAttemptCompleted: ({ message }) =>
useComponentBar
? handleSaveCipherConfirmation(message)
: handleSaveCipherAttemptCompletedMessage(message),
}; };
globalThis.addEventListener("load", load); globalThis.addEventListener("load", load);
function load() { function load() {
setupWindowMessageListener(); setupWindowMessageListener();
sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => { sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => {
@@ -35,7 +41,6 @@ function load() {
applyNotificationBarStyle(); applyNotificationBarStyle();
}); });
} }
function applyNotificationBarStyle() { function applyNotificationBarStyle() {
if (!useComponentBar) { if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -44,16 +49,8 @@ function applyNotificationBarStyle() {
postMessageToParent({ command: "initNotificationBar" }); postMessageToParent({ command: "initNotificationBar" });
} }
function initNotificationBar(message: NotificationBarWindowMessage) { function getI18n() {
const { initData } = message; return {
if (!initData) {
return;
}
notificationBarIframeInitData = initData;
const { isVaultLocked, theme } = notificationBarIframeInitData;
const i18n = {
appName: chrome.i18n.getMessage("appName"), appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"), close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"), never: chrome.i18n.getMessage("never"),
@@ -74,20 +71,30 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
updateLoginPrompt: "Update existing login?", updateLoginPrompt: "Update existing login?",
loginSaveSuccess: "Login saved", loginSaveSuccess: "Login saved",
loginSaveSuccessDetails: "Login saved to Bitwarden.", loginSaveSuccessDetails: "Login saved to Bitwarden.",
loginUpdateSuccess: "Login saved", loginUpdateSuccess: "Login updated",
loginUpdateSuccessDetails: "Login updated in Bitwarden.", loginUpdateSuccessDetails: "Login updated in Bitwarden.",
saveFailure: "Error saving", saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item", 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) { if (useComponentBar) {
document.body.innerHTML = ""; document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node. // 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()); 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) => { sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
// @TODO use context to avoid prop drilling // @TODO use context to avoid prop drilling
@@ -105,77 +112,71 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body, 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 setupLogoLink(i18n);
? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL("images/icon38.png");
setupLogoLink(i18n); // i18n for "Add" template
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
// i18n for "Add" template const neverButton = addTemplate.content.getElementById("never-save");
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement; neverButton.textContent = i18n.never;
const neverButton = addTemplate.content.getElementById("never-save"); const selectFolder = addTemplate.content.getElementById("select-folder");
neverButton.textContent = i18n.never; selectFolder.hidden = isVaultLocked || removeIndividualVault();
selectFolder.setAttribute("aria-label", i18n.folder);
const selectFolder = addTemplate.content.getElementById("select-folder"); const addButton = addTemplate.content.getElementById("add-save");
selectFolder.hidden = isVaultLocked || removeIndividualVault(); addButton.textContent = i18n.notificationAddSave;
selectFolder.setAttribute("aria-label", i18n.folder);
const addButton = addTemplate.content.getElementById("add-save"); const addEditButton = addTemplate.content.getElementById("add-edit");
addButton.textContent = i18n.notificationAddSave; // 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"); addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
// 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; // i18n for "Change" (update password) template
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
// i18n for "Change" (update password) template const changeButton = changeTemplate.content.getElementById("change-save");
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement; changeButton.textContent = i18n.notificationChangeSave;
const changeButton = changeTemplate.content.getElementById("change-save"); const changeEditButton = changeTemplate.content.getElementById("change-edit");
changeButton.textContent = i18n.notificationChangeSave; changeEditButton.textContent = i18n.notificationEdit;
const changeEditButton = changeTemplate.content.getElementById("change-edit"); changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
changeEditButton.textContent = i18n.notificationEdit;
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 unlockButton = unlockTemplate.content.getElementById("unlock-vault");
const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement; unlockButton.textContent = i18n.notificationUnlock;
const unlockButton = unlockTemplate.content.getElementById("unlock-vault"); unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
unlockButton.textContent = i18n.notificationUnlock;
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 notificationType = initData.type;
const closeButton = document.getElementById("close-button"); if (notificationType === "add") {
closeButton.title = i18n.close; handleTypeAdd();
} else if (notificationType === "change") {
handleTypeChange();
} else if (notificationType === "unlock") {
handleTypeUnlock();
}
const notificationType = initData.type; closeButton.addEventListener("click", handleCloseNotification);
if (notificationType === "add") {
handleTypeAdd();
} else if (notificationType === "change") {
handleTypeChange();
} else if (notificationType === "unlock") {
handleTypeUnlock();
}
closeButton.addEventListener("click", handleCloseNotification); globalThis.addEventListener("resize", adjustHeight);
adjustHeight();
globalThis.addEventListener("resize", adjustHeight);
adjustHeight();
function handleCloseNotification(e: Event) {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
} }
function handleEditOrUpdateAction(e: Event) { function handleEditOrUpdateAction(e: Event) {
const notificationType = initData.type; const notificationType = initData.type;
@@ -183,6 +184,12 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false); notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
} }
} }
function handleCloseNotification(e: Event) {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
}
function handleSaveAction(e: Event) { function handleSaveAction(e: Event) {
e.preventDefault(); 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() { function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement); setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
@@ -395,6 +423,14 @@ function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]
return 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() { function setNotificationBarTheme() {
const theme = getTheme(globalThis, notificationBarIframeInitData.theme); const theme = getTheme(globalThis, notificationBarIframeInitData.theme);