From 4a01c8bb171da95746c9d37015098e1164370b0a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 24 Apr 2025 16:15:27 -0400 Subject: [PATCH] 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 * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/notification/bar.ts Co-authored-by: Jonathan Prusik * Update apps/browser/src/autofill/content/components/cipher/cipher-action.ts Co-authored-by: Jonathan Prusik --------- Co-authored-by: Jonathan Prusik --- apps/browser/src/_locales/en/messages.json | 46 ++++---- .../background/notification.background.ts | 100 +++++++++++++----- .../content/components/buttons/edit-button.ts | 1 + .../components/cipher/cipher-action.ts | 2 +- .../content/components/icons/pencil-square.ts | 2 +- .../components/notification/container.ts | 4 +- apps/browser/src/autofill/notification/bar.ts | 5 +- 7 files changed, 104 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index d1d05a4e85..4f83b07506 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1071,6 +1071,10 @@ }, "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": { "message": "New notification" }, @@ -1110,12 +1114,12 @@ "message": "Update login", "description": "Button text for updating an existing login entry." }, - "saveLoginPrompt": { - "message": "Save login?", + "saveLogin": { + "message": "Save login", "description": "Prompt asking the user if they want to save their login details." }, - "updateLoginPrompt": { - "message": "Update existing login?", + "updateLogin": { + "message": "Update existing login", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1128,24 +1132,24 @@ }, "loginUpdateTaskSuccess": { "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", - "placeholders": { - "organization": { - "content": "$1" - } - }, - "description": "Shown to user after login is updated." + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", - "placeholders": { - "organization": { - "content": "$1" - }, - "task_count": { - "content": "$2" - } + "placeholders": { + "organization": { + "content": "$1" }, - "description": "Shown to user after login is updated." + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { "message": "Change next password", @@ -2518,8 +2522,8 @@ "example": "Acme Corp" }, "count": { - "content": "$2", - "example": "2" + "content": "$2", + "example": "2" } } }, @@ -5224,4 +5228,4 @@ "secureDevicesBody": { "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." } -} +} \ No newline at end of file diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0b9c624498..9083f15d4f 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -163,6 +163,7 @@ export default class NotificationBackground { * 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. * If no active tab or URL is found, it returns an empty array. + * If new login, returns a preview of the cipher. * * @returns {Promise} */ @@ -175,53 +176,94 @@ export default class NotificationBackground { firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)), ]); + if (!currentTab?.url || !activeUserId) { + return []; + } + const [decryptedCiphers, organizations] = await Promise.all([ - this.cipherService.getAllDecryptedForUrl(currentTab?.url, activeUserId), + this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId), firstValueFrom(this.organizationService.organizations$(activeUserId)), ]); const iconsServerUrl = env.getIconsUrl(); - const toNotificationData = (view: CipherView): NotificationCipherData => { - const { id, name, reprompt, favorite, login, organizationId } = view; + const getOrganizationType = (orgId?: string) => + 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, + ); - const organizationCategories: OrganizationCategory[] = []; + 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[] = []; + + if (organizationType != null) { if ( [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( - type, + organizationType, ) ) { organizationCategories.push(OrganizationCategories.business); } - if ([ProductTierType.Families, ProductTierType.Free].includes(type)) { + + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { organizationCategories.push(OrganizationCategories.family); } - - return { - id, - name, - type: CipherType.Login, - reprompt, - favorite, - ...(organizationCategories.length ? { organizationCategories } : {}), - icon: buildCipherIcon(iconsServerUrl, view, showFavicons), - 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); + return { + id, + name, + type: CipherType.Login, + reprompt, + favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), + icon: buildCipherIcon(iconsServerUrl, view, showFavicons), + login: login && { username: login.username }, + }; } /** diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index 67221f5be1..a0037146db 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -21,6 +21,7 @@ export function EditButton({