Skip to content

Commit

Permalink
PM-18276-Connect confirmation UI (#13498)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dan-livefront authored Feb 26, 2025
1 parent d999d91 commit 9aee5f1
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}) {
Expand All @@ -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;
Expand Down
174 changes: 105 additions & 69 deletions apps/browser/src/autofill/notification/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -22,20 +23,24 @@ 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) => {
useComponentBar = flagValue;
applyNotificationBarStyle();
});
}

function applyNotificationBarStyle() {
if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand All @@ -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"),
Expand All @@ -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
Expand All @@ -105,84 +112,84 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body,
);
});
}

setNotificationBarTheme();
} else {
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;
e.preventDefault();
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
}
}
function handleCloseNotification(e: Event) {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
}

function handleSaveAction(e: Event) {
e.preventDefault();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 9aee5f1

Please sign in to comment.