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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user