mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +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:
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function CipherAction({
|
|||||||
})
|
})
|
||||||
: EditButton({
|
: EditButton({
|
||||||
buttonAction: handleAction,
|
buttonAction: handleAction,
|
||||||
buttonText: i18n.notificationEdit,
|
buttonText: i18n.notificationEditTooltip,
|
||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user