1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

Pm 18493 pass relevant cipher name into confirmation UI (#13570)

* 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

* PM-18493 initial wip commit

* fix types and story

* edit tests to account for sendmessagewithdata

* add tests and return id on new add/save

* function name
This commit is contained in:
Daniel Riera
2025-02-28 13:14:15 -05:00
committed by GitHub
parent 40f7a0d73f
commit f12456bd3e
10 changed files with 102 additions and 23 deletions

View File

@@ -1070,6 +1070,24 @@
"notificationAddSave": { "notificationAddSave": {
"message": "Save" "message": "Save"
}, },
"loginSaveSuccessDetails": {
"message": "$USERNAME$ saved to Bitwarden.",
"placeholders": {
"username": {
"content": "$1"
}
},
"description": "Shown to user after login is saved."
},
"loginUpdatedSuccessDetails": {
"message": "$USERNAME$ updated in Bitwarden.",
"placeholders": {
"username": {
"content": "$1"
}
},
"description": "Shown to user after login is updated."
},
"enableChangedPasswordNotification": { "enableChangedPasswordNotification": {
"message": "Ask to update existing login" "message": "Ask to update existing login"
}, },

View File

@@ -100,6 +100,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise<void>;
bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>; bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;

View File

@@ -812,7 +812,10 @@ describe("NotificationBackground", () => {
newPassword: "newPassword", newPassword: "newPassword",
}); });
notificationBackground["notificationQueue"] = [queueMessage]; notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>(); const cipherView = mock<CipherView>({
id: "testId",
login: { username: "testUser" },
});
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
sendMockExtensionMessage(message, sender); sendMockExtensionMessage(message, sender);
@@ -828,9 +831,14 @@ describe("NotificationBackground", () => {
"testId", "testId",
); );
expect(updateWithServerSpy).toHaveBeenCalled(); expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
command: "saveCipherAttemptCompleted", sender.tab,
}); "saveCipherAttemptCompleted",
{
username: cipherView.login.username,
cipherId: cipherView.id,
},
);
}); });
it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => { it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => {
@@ -976,11 +984,16 @@ describe("NotificationBackground", () => {
}); });
notificationBackground["notificationQueue"] = [queueMessage]; notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({ const cipherView = mock<CipherView>({
id: "testId",
login: { username: "test", password: "password" }, login: { username: "test", password: "password" },
}); });
folderExistsSpy.mockResolvedValueOnce(false); folderExistsSpy.mockResolvedValueOnce(false);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView); convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined); editItemSpy.mockResolvedValueOnce(undefined);
cipherEncryptSpy.mockResolvedValueOnce({
...cipherView,
id: "testId",
});
sendMockExtensionMessage(message, sender); sendMockExtensionMessage(message, sender);
await flushPromises(); await flushPromises();
@@ -991,9 +1004,14 @@ describe("NotificationBackground", () => {
); );
expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView, "testId"); expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView, "testId");
expect(createWithServerSpy).toHaveBeenCalled(); expect(createWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
command: "saveCipherAttemptCompleted", sender.tab,
}); "saveCipherAttemptCompleted",
{
username: cipherView.login.username,
cipherId: cipherView.id,
},
);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" });
}); });

View File

@@ -87,6 +87,7 @@ export default class NotificationBackground {
getWebVaultUrlForNotification: () => this.getWebVaultUrl(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
notificationRefreshFlagValue: () => this.getNotificationFlag(), notificationRefreshFlagValue: () => this.getNotificationFlag(),
bgGetDecryptedCiphers: () => this.getNotificationCipherData(), bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
}; };
constructor( constructor(
@@ -594,7 +595,10 @@ export default class NotificationBackground {
const cipher = await this.cipherService.encrypt(newCipher, activeUserId); const cipher = await this.cipherService.encrypt(newCipher, activeUserId);
try { try {
await this.cipherService.createWithServer(cipher); await this.cipherService.createWithServer(cipher);
await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" }); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: String(queueMessage?.username),
cipherId: String(cipher?.id),
});
await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" });
} catch (error) { } catch (error) {
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
@@ -630,15 +634,16 @@ export default class NotificationBackground {
await BrowserApi.tabSendMessage(tab, { command: "editedCipher" }); await BrowserApi.tabSendMessage(tab, { command: "editedCipher" });
return; return;
} }
const cipher = await this.cipherService.encrypt(cipherView, userId); const cipher = await this.cipherService.encrypt(cipherView, userId);
try { try {
// We've only updated the password, no need to broadcast editedCipher message
await this.cipherService.updateWithServer(cipher); await this.cipherService.updateWithServer(cipher);
await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" }); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
username: String(cipherView?.login?.username),
cipherId: String(cipherView?.id),
});
} catch (error) { } catch (error) {
await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", {
error: String(error.message), error: String(error?.message),
}); });
} }
} }
@@ -663,6 +668,16 @@ export default class NotificationBackground {
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
} }
private async openVault(
message: NotificationBackgroundExtensionMessage,
senderTab: chrome.tabs.Tab,
) {
if (!message.cipherId) {
await this.openAddEditVaultItemPopout(senderTab);
}
await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId });
}
private async folderExists(folderId: string, userId: UserId) { private async folderExists(folderId: string, userId: UserId) {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false; return false;

View File

@@ -7,7 +7,7 @@ import { NotificationConfirmationBody } from "../../notification/confirmation";
type Args = { type Args = {
buttonText: string; buttonText: string;
confirmationMessage: string; confirmationMessage: string;
handleClick: () => void; handleOpenVault: () => void;
theme: Theme; theme: Theme;
error: string; error: string;
}; };

View File

@@ -19,18 +19,22 @@ import {
export function NotificationConfirmationContainer({ export function NotificationConfirmationContainer({
error, error,
handleCloseNotification, handleCloseNotification,
handleOpenVault,
i18n, i18n,
theme = ThemeTypes.Light, theme = ThemeTypes.Light,
type, type,
username,
}: NotificationBarIframeInitData & { }: NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void; handleCloseNotification: (e: Event) => void;
handleOpenVault: (e: Event) => void;
} & { } & {
error: string; error: string;
i18n: { [key: string]: string }; i18n: { [key: string]: string };
type: NotificationType; type: NotificationType;
username: string;
}) { }) {
const headerMessage = getHeaderMessage(i18n, type, error); const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, username, type, error);
const buttonText = error ? i18n.newItem : i18n.view; const buttonText = error ? i18n.newItem : i18n.view;
return html` return html`
@@ -41,9 +45,10 @@ export function NotificationConfirmationContainer({
theme, theme,
})} })}
${NotificationConfirmationBody({ ${NotificationConfirmationBody({
error: error,
buttonText, buttonText,
confirmationMessage, confirmationMessage,
error: error,
handleOpenVault,
theme, theme,
})} })}
</div> </div>
@@ -68,14 +73,21 @@ const notificationContainerStyles = (theme: Theme) => css`
function getConfirmationMessage( function getConfirmationMessage(
i18n: { [key: string]: string }, i18n: { [key: string]: string },
username: string,
type?: NotificationType, type?: NotificationType,
error?: string, error?: string,
) { ) {
const loginSaveSuccessDetails = chrome.i18n.getMessage("loginSaveSuccessDetails", [username]);
const loginUpdatedSuccessDetails = chrome.i18n.getMessage("loginUpdatedSuccessDetails", [
username,
]);
if (error) { if (error) {
return i18n.saveFailureDetails; return i18n.saveFailureDetails;
} }
return type === "add" ? i18n.loginSaveSuccessDetails : i18n.loginUpdateSuccessDetails; return type === "add" ? loginSaveSuccessDetails : loginUpdatedSuccessDetails;
} }
function getHeaderMessage( function getHeaderMessage(
i18n: { [key: string]: string }, i18n: { [key: string]: string },
type?: NotificationType, type?: NotificationType,

View File

@@ -1,7 +1,7 @@
import createEmotion from "@emotion/css/create-instance"; import createEmotion from "@emotion/css/create-instance";
import { html } from "lit"; import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { Theme } from "@bitwarden/common/platform/enums";
import { themes } from "../constants/styles"; import { themes } from "../constants/styles";
import { PartyHorn, Warning } from "../icons"; import { PartyHorn, Warning } from "../icons";
@@ -18,12 +18,14 @@ export function NotificationConfirmationBody({
buttonText, buttonText,
error, error,
confirmationMessage, confirmationMessage,
theme = ThemeTypes.Light, theme,
handleOpenVault,
}: { }: {
error?: string; error?: string;
buttonText: string; buttonText: string;
confirmationMessage: string; confirmationMessage: string;
theme: Theme; theme: Theme;
handleOpenVault: (e: Event) => void;
}) { }) {
const IconComponent = !error ? PartyHorn : Warning; const IconComponent = !error ? PartyHorn : Warning;
return html` return html`
@@ -31,7 +33,7 @@ export function NotificationConfirmationBody({
<div class=${iconContainerStyles(error)}>${IconComponent({ theme })}</div> <div class=${iconContainerStyles(error)}>${IconComponent({ theme })}</div>
${confirmationMessage && buttonText ${confirmationMessage && buttonText
? NotificationConfirmationMessage({ ? NotificationConfirmationMessage({
handleClick: () => {}, handleClick: handleOpenVault,
confirmationMessage, confirmationMessage,
theme, theme,
buttonText, buttonText,

View File

@@ -193,6 +193,8 @@ async function loadNotificationBar() {
{ {
command: "saveCipherAttemptCompleted", command: "saveCipherAttemptCompleted",
error: msg.data?.error, error: msg.data?.error,
username: msg.data?.username,
cipherId: msg.data?.cipherId,
}, },
"*", "*",
); );

View File

@@ -19,10 +19,11 @@ type NotificationBarIframeInitData = {
}; };
type NotificationBarWindowMessage = { type NotificationBarWindowMessage = {
[key: string]: any;
command: string; command: string;
error?: string; error?: string;
initData?: NotificationBarIframeInitData; initData?: NotificationBarIframeInitData;
username?: string;
cipherId?: string;
}; };
type NotificationBarWindowMessageHandlers = { type NotificationBarWindowMessageHandlers = {

View File

@@ -55,6 +55,8 @@ function getI18n() {
close: chrome.i18n.getMessage("close"), close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"), never: chrome.i18n.getMessage("never"),
folder: chrome.i18n.getMessage("folder"), folder: chrome.i18n.getMessage("folder"),
loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"),
loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"),
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"), notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
notificationEdit: chrome.i18n.getMessage("edit"), notificationEdit: chrome.i18n.getMessage("edit"),
@@ -70,9 +72,7 @@ function getI18n() {
saveLoginPrompt: "Save login?", saveLoginPrompt: "Save login?",
updateLoginPrompt: "Update existing login?", updateLoginPrompt: "Update existing login?",
loginSaveSuccess: "Login saved", loginSaveSuccess: "Login saved",
loginSaveSuccessDetails: "Login saved to Bitwarden.",
loginUpdateSuccess: "Login updated", loginUpdateSuccess: "Login updated",
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", newItem: "New item",
@@ -289,9 +289,17 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
); );
} }
function openViewVaultItemPopout(e: Event, cipherId: string) {
e.preventDefault();
sendPlatformMessage({
command: "bgOpenVault",
cipherId,
});
}
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
const { theme, type } = notificationBarIframeInitData; const { theme, type } = notificationBarIframeInitData;
const { error } = message; const { error, username, cipherId } = message;
const i18n = getI18n(); const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme); const resolvedTheme = getResolvedTheme(theme);
@@ -305,6 +313,8 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
handleCloseNotification, handleCloseNotification,
i18n, i18n,
error, error,
username,
handleOpenVault: (e) => openViewVaultItemPopout(e, cipherId),
}), }),
document.body, document.body,
); );