1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

PM-20391 UX: Saving new login when none exist (#14406)

* PM-20391 UX: Saving new login when none exist

* Update apps/browser/src/_locales/en/messages.json

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* Update apps/browser/src/_locales/en/messages.json

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* Update apps/browser/src/autofill/notification/bar.ts

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

* Update apps/browser/src/autofill/content/components/cipher/cipher-action.ts

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>

---------

Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com>
This commit is contained in:
Daniel Riera
2025-04-24 16:15:27 -04:00
committed by GitHub
parent 116751d4ca
commit 4a01c8bb17
7 changed files with 104 additions and 56 deletions

View File

@@ -1071,6 +1071,10 @@
}, },
"description": "Aria label for the view button in notification bar confirmation message" "description": "Aria label for the view button in notification bar confirmation message"
}, },
"notificationEditTooltip": {
"message": "Edit before saving",
"description": "Tooltip and Aria label for edit button on cipher item"
},
"newNotification": { "newNotification": {
"message": "New notification" "message": "New notification"
}, },
@@ -1110,12 +1114,12 @@
"message": "Update login", "message": "Update login",
"description": "Button text for updating an existing login entry." "description": "Button text for updating an existing login entry."
}, },
"saveLoginPrompt": { "saveLogin": {
"message": "Save login?", "message": "Save login",
"description": "Prompt asking the user if they want to save their login details." "description": "Prompt asking the user if they want to save their login details."
}, },
"updateLoginPrompt": { "updateLogin": {
"message": "Update existing login?", "message": "Update existing login",
"description": "Prompt asking the user if they want to update an existing login entry." "description": "Prompt asking the user if they want to update an existing login entry."
}, },
"loginSaveSuccess": { "loginSaveSuccess": {

View File

@@ -163,6 +163,7 @@ export default class NotificationBackground {
* Gets the current active tab and retrieves the relevant decrypted cipher * Gets the current active tab and retrieves the relevant decrypted cipher
* for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object. * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects or a singular object.
* If no active tab or URL is found, it returns an empty array. * If no active tab or URL is found, it returns an empty array.
* If new login, returns a preview of the cipher.
* *
* @returns {Promise<NotificationCipherData[]>} * @returns {Promise<NotificationCipherData[]>}
*/ */
@@ -175,29 +176,83 @@ export default class NotificationBackground {
firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)), firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)),
]); ]);
if (!currentTab?.url || !activeUserId) {
return [];
}
const [decryptedCiphers, organizations] = await Promise.all([ const [decryptedCiphers, organizations] = await Promise.all([
this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId), this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId),
firstValueFrom(this.organizationService.organizations$(activeUserId)), firstValueFrom(this.organizationService.organizations$(activeUserId)),
]); ]);
const iconsServerUrl = env.getIconsUrl(); const iconsServerUrl = env.getIconsUrl();
const toNotificationData = (view: CipherView): NotificationCipherData => { const getOrganizationType = (orgId?: string) =>
const { id, name, reprompt, favorite, login, organizationId } = view; organizations.find((org) => org.id === orgId)?.productTierType;
const type = organizations.find((org) => org.id === organizationId)?.productTierType; const cipherQueueMessage = this.notificationQueue.find(
(message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage =>
message.type === NotificationQueueMessageType.ChangePassword ||
message.type === NotificationQueueMessageType.AddLogin,
);
if (cipherQueueMessage) {
const cipherView =
cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword
? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId)
: this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
const organizationType = getOrganizationType(cipherView.organizationId);
return [
this.convertToNotificationCipherData(
cipherView,
iconsServerUrl,
showFavicons,
organizationType,
),
];
}
return decryptedCiphers.map((view) =>
this.convertToNotificationCipherData(
view,
iconsServerUrl,
showFavicons,
getOrganizationType(view.organizationId),
),
);
}
/**
* Converts a CipherView and organization type into a NotificationCipherData object
* for use in the notification bar.
*
* @returns A NotificationCipherData object containing the relevant cipher information.
*/
convertToNotificationCipherData(
view: CipherView,
iconsServerUrl: string,
showFavicons: boolean,
organizationType?: ProductTierType,
): NotificationCipherData {
const { id, name, reprompt, favorite, login } = view;
const organizationCategories: OrganizationCategory[] = []; const organizationCategories: OrganizationCategory[] = [];
if (organizationType != null) {
if ( if (
[ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes(
type, organizationType,
) )
) { ) {
organizationCategories.push(OrganizationCategories.business); organizationCategories.push(OrganizationCategories.business);
} }
if ([ProductTierType.Families, ProductTierType.Free].includes(type)) {
if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) {
organizationCategories.push(OrganizationCategories.family); organizationCategories.push(OrganizationCategories.family);
} }
}
return { return {
id, id,
@@ -209,19 +264,6 @@ export default class NotificationBackground {
icon: buildCipherIcon(iconsServerUrl, view, showFavicons), icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
login: login && { username: login.username }, login: login && { username: login.username },
}; };
};
const changeItem = this.notificationQueue.find(
(message): message is AddChangePasswordQueueMessage =>
message.type === NotificationQueueMessageType.ChangePassword,
);
if (changeItem) {
const cipherView = await this.getDecryptedCipherById(changeItem.cipherId, activeUserId);
return [toNotificationData(cipherView)];
}
return decryptedCiphers.map(toNotificationData);
} }
/** /**

View File

@@ -21,6 +21,7 @@ export function EditButton({
<button <button
type="button" type="button"
title=${buttonText} title=${buttonText}
aria-label=${buttonText}
class=${editButtonStyles({ disabled, theme })} class=${editButtonStyles({ disabled, theme })}
@click=${(event: Event) => { @click=${(event: Event) => {
if (!disabled) { if (!disabled) {

View File

@@ -25,7 +25,7 @@ export function CipherAction({
}) })
: EditButton({ : EditButton({
buttonAction: handleAction, buttonAction: handleAction,
buttonText: i18n.notificationEdit, buttonText: i18n.notificationEditTooltip,
theme, theme,
}); });
} }

View File

@@ -8,7 +8,7 @@ export function PencilSquare({ color, disabled, theme }: IconProps) {
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
return html` return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<path <path
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))} class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
d="M11.013.677a1.75 1.75 0 0 1 2.474 0l.836.836a1.75 1.75 0 0 1 0 2.475L9.03 9.28a.75.75 0 0 1-.348.197l-3 .75a.75.75 0 0 1-.91-.91l.75-3a.75.75 0 0 1 .198-.348L11.013.677Zm1.414 1.06a.25.25 0 0 0-.354 0l-.646.647a.75.75 0 0 1 .103.086l1 1a.751.751 0 0 1 .087.103l.646-.646a.25.25 0 0 0 0-.353l-.836-.836Zm-.854 2.88a.752.752 0 0 1-.103-.087l-1-1a.756.756 0 0 1-.087-.103L6.928 6.884 6.531 8.47l1.586-.397 3.456-3.456Z" d="M11.013.677a1.75 1.75 0 0 1 2.474 0l.836.836a1.75 1.75 0 0 1 0 2.475L9.03 9.28a.75.75 0 0 1-.348.197l-3 .75a.75.75 0 0 1-.91-.91l.75-3a.75.75 0 0 1 .198-.348L11.013.677Zm1.414 1.06a.25.25 0 0 0-.354 0l-.646.647a.75.75 0 0 1 .103.086l1 1a.751.751 0 0 1 .087.103l.646-.646a.25.25 0 0 0 0-.353l-.836-.836Zm-.854 2.88a.752.752 0 0 1-.103-.087l-1-1a.756.756 0 0 1-.087-.103L6.928 6.884 6.531 8.47l1.586-.397 3.456-3.456Z"

View File

@@ -96,9 +96,9 @@ const notificationContainerStyles = (theme: Theme) => css`
function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationType) { function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationType) {
switch (type) { switch (type) {
case NotificationTypes.Add: case NotificationTypes.Add:
return i18n.saveAsNewLoginAction; return i18n.saveLogin;
case NotificationTypes.Change: case NotificationTypes.Change:
return i18n.updateLoginPrompt; return i18n.updateLogin;
case NotificationTypes.Unlock: case NotificationTypes.Unlock:
return ""; return "";
default: default:

View File

@@ -70,6 +70,7 @@ function getI18n() {
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"), notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
notificationEdit: chrome.i18n.getMessage("edit"), notificationEdit: chrome.i18n.getMessage("edit"),
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"), notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
@@ -77,10 +78,10 @@ function getI18n() {
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"), saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
saveFailure: chrome.i18n.getMessage("saveFailure"), saveFailure: chrome.i18n.getMessage("saveFailure"),
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"), saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
saveLoginPrompt: chrome.i18n.getMessage("saveLoginPrompt"), saveLogin: chrome.i18n.getMessage("saveLogin"),
typeLogin: chrome.i18n.getMessage("typeLogin"), typeLogin: chrome.i18n.getMessage("typeLogin"),
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"), updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"), updateLogin: chrome.i18n.getMessage("updateLogin"),
vault: chrome.i18n.getMessage("vault"), vault: chrome.i18n.getMessage("vault"),
view: chrome.i18n.getMessage("view"), view: chrome.i18n.getMessage("view"),
}; };